+;
+; where is the name of the section. This name can be used
+; in CODE: syntax of description file
+;
+; consists of code. Use \t for a tab character. Other
+; substitution keywords are:
+;
+; %CONTROL:% for a control property
+; %FORM:% for a form property
+;
+; ie: %CONTROL:NAME% for control's name
+; %CONTROL:CLASSNAME% for control's class name
+;
+; %INITPARAMS% for the parameters of the INIT method
+;
+; Note: semicolon (';') in first column designates a comment line
+
+
+[ClassDeclaration]
+class %FORM:NAME% inherit %FORM:CLASSNAME%
+
+[Activate]
+method Activate(oEvent) class %FORM:NAME%
+\tsuper:Activate(oEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ButtonClick]
+method ButtonClick(oControlEvent) class %FORM:NAME%
+\tlocal oControl as Control
+\toControl := IIf(oControlEvent == NULL_OBJECT, NULL_OBJECT, oControlEvent:Control)
+\tsuper:ButtonClick(oControlEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ButtonDoubleClick]
+method ButtonDoubleClick(oControlEvent) class %FORM:NAME%
+\tlocal oControl as Control
+\toControl := IIf(oControlEvent == NULL_OBJECT, NULL_OBJECT, oControlEvent:Control)
+\tsuper:ButtonDoubleClick(oControlEvent)
+\t//Put your changes here
+\treturn NIL
+
+[Close]
+method Close(oEvent) class %FORM:NAME%
+\tsuper:Close(oEvent)
+\t//Put your changes here
+\treturn NIL
+
+[Deactivate]
+method Deactivate(oEvent) class %FORM:NAME%
+\tsuper:Deactivate(oEvent)
+\t//Put your changes here
+\treturn NIL
+
+[Draw]
+method Draw(oDrawObject) class %FORM:NAME%
+\tsuper:Draw(oDrawObject)
+\t//Put your changes here
+return NIL
+
+[EditChange]
+method EditChange(oControlEvent) class %FORM:NAME%
+\tlocal oControl as Control
+\toControl := IIf(oControlEvent == NULL_OBJECT, NULL_OBJECT, oControlEvent:Control)
+\tsuper:EditChange(oControlEvent)
+\t//Put your changes here
+\treturn NIL\r\n
+
+[EditFocusChange]
+method EditFocusChange(oEditFocusChangeEvent) class %FORM:NAME%
+\tlocal oControl as Control
+\tlocal lGotFocus as logic
+\toControl := IIf(oEditFocusChangeEvent == NULL_OBJECT, NULL_OBJECT, oEditFocusChangeEvent:Control)
+\tlGotFocus := IIf(oEditFocusChangeEvent == NULL_OBJECT, FALSE, oEditFocusChangeEvent:GotFocus)
+\tsuper:EditFocusChange(oEditFocusChangeEvent)
+\t//Put your changes here
+\treturn NIL
+
+[EditScroll]
+method EditScroll(oControlEvent) class %FORM:NAME%
+\tlocal oControl as Control
+\toControl := IIf(oControlEvent == NULL_OBJECT, NULL_OBJECT, oControlEvent:Control)
+\tsuper:EditScroll(oControlEvent)
+\t//Put your changes here
+\treturn NIL
+
+[Expose]
+method Expose(oExposeEvent) class %FORM:NAME%
+\tlocal oBoundingBox as BoundingBox
+\toBoundingBox := IIf(oExposeEvent == NULL_OBJECT, NULL_OBJECT, oExposeEvent:ExposedArea)
+\tsuper:Expose(oExposeEvent)
+\t//Put your changes here
+\treturn NIL\r\n
+
+[FocusChange]
+method FocusChange(oFocusChangeEvent) class %FORM:NAME%
+\tlocal lGotFocus as logic
+\tlGotFocus := IIf(oFocusChangeEvent == NULL_OBJECT, FALSE, oFocusChangeEvent:GotFocus)
+\tsuper:FocusChange(oFocusChangeEvent)
+\t//Put your changes here
+\treturn NIL
+
+[PreInit]
+method PreInit(%INITPARAMS%) class %FORM:NAME%
+\t//Put your PreInit additions here
+\treturn NIL
+
+[PostInit]
+method PostInit(%INITPARAMS%) class %FORM:NAME%
+\t//Put your PostInit additions here
+\treturn NIL
+
+[PreInitCall]
+; this section is inserted at the end of the INIT method of the form
+; use %INITPARAMS% to insert the parameters of INIT
+self:PreInit(%INITPARAMS%)
+
+[PostInitCall]
+; this section is inserted at the end of the INIT method the form
+; use %INITPARAMS% to insert the parameters of INIT
+self:PostInit(%INITPARAMS%)
+
+[HelpRequest]
+method HelpRequest(oHelpRequestEvent) class %FORM:NAME%
+\tsuper:HelpRequest(oHelpRequestEvent)
+\t//Put your changes here
+\treturn NIL
+
+[HorizontalScroll]
+method HorizontalScroll(oScrollEvent) class %FORM:NAME%
+\tsuper:HorizontalScroll(oScrollEvent)
+\t//Put your changes here
+\treturn NIL
+
+[Keydown]
+method KeyDown(oKeyEvent) class %FORM:NAME%
+\tlocal nKeyCode as int
+\tnKeyCode := IIf(oKeyEvent = NULL_OBJECT, 0, IIf(IsNil(oKeyEvent:ASCIIChar), oKeyEvent:KeyCode, oKeyEvent:ASCIIChar))
+\tsuper:KeyDown(oKeyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[Keyup]
+method KeyUp(oKeyEvent) class %FORM:NAME%
+\tlocal nKeyCode as int
+\tnKeyCode := IIf(oKeyEvent = NULL_OBJECT, 0, IIf(IsNil(oKeyEvent:ASCIIChar), oKeyEvent:KeyCode, oKeyEvent:ASCIIChar))
+\tsuper:KeyUp(oKeyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ListBoxClick]
+method ListBoxClick(oControlEvent) class %FORM:NAME%
+\tlocal oControl as Control
+\toControl := IIf(oControlEvent == NULL_OBJECT, NULL_OBJECT, oControlEvent:Control)
+\tsuper:ListBoxClick(oControlEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ListBoxSelect]
+method ListBoxSelect(oControlEvent) class %FORM:NAME%
+\tlocal oControl as Control
+\toControl := IIf(oControlEvent == NULL_OBJECT, NULL_OBJECT, oControlEvent:Control)
+\tsuper:ListBoxSelect(oControlEvent)
+\t//Put your changes here
+\treturn NIL
+
+[MenuCommand]
+method MenuCommand(oMenuCommandEvent) class %FORM:NAME%
+\tsuper:MenuCommand(oMenuCommandEvent)
+\t//Put your changes here
+\treturn NIL
+
+[MenuInit]
+method MenuInit(oMenuInitEvent) class %FORM:NAME%
+\tsuper:MenuInit(oMenuInitEvent)
+\t//Put your changes here
+\treturn NIL
+
+[MenuSelect]
+method MenuSelect(oMenuSelectEvent) class %FORM:NAME%
+\tsuper:MenuSelect(oMenuSelectEvent)
+\t//Put your changes here
+\treturn NIL
+
+[MouseButtonDoubleClick]
+method MouseButtonDoubleClick(oMouseEvent) class %FORM:NAME%
+\tlocal nButtonID as int
+\tnButtonID := IIf(oMouseEvent == NULL_OBJECT, 0, oMouseEvent:ButtonID)
+\tsuper:MouseButtonDoubleClick(oMouseEvent)
+\t//Put your changes here
+\treturn NIL
+
+[MouseButtonDown]
+method MouseButtonDown(oMouseEvent) class %FORM:NAME%
+\tlocal nButtonID as int
+\tnButtonID := IIf(oMouseEvent == NULL_OBJECT, 0, oMouseEvent:ButtonID)
+\tsuper:MouseButtonDown(oMouseEvent)
+\t//Put your changes here
+\treturn NIL
+
+[MouseButtonUp]
+method MouseButtonUp(oMouseEvent) class %FORM:NAME%
+\tlocal nButtonID as int
+\tnButtonID := IIf(oMouseEvent == NULL_OBJECT, 0, oMouseEvent:ButtonID)
+\tsuper:MouseButtonUp(oMouseEvent)
+\t//Put your changes here
+\treturn NIL
+
+[MouseDrag]
+method MouseDrag(oMouseEvent) class %FORM:NAME%
+\tlocal nButtonID as int
+\tnButtonID := IIf(oMouseEvent == NULL_OBJECT, 0, oMouseEvent:ButtonID)
+\tsuper:MouseDrag(oMouseEvent)
+\t//Put your changes here
+\treturn NIL
+
+[MouseMove]
+method MouseMove(oMouseEvent) class %FORM:NAME%
+\tlocal nButtonID as int
+\tnButtonID := IIf(oMouseEvent == NULL_OBJECT, 0, oMouseEvent:ButtonID)
+\tsuper:MouseMove(oMouseEvent)
+\t//Put your changes here
+\treturn NIL
+
+[Move]
+method Move(oMoveEvent) class %FORM:NAME%
+\tsuper:Move(oMoveEvent)
+\t//Put your changes here
+\treturn NIL
+
+[Notify]
+method Notify(kNotifyName, uDescription) class %FORM:NAME%
+\tlocal uValue as usual
+\tuValue := super:Notify(kNotifyName, uDescription)
+\t//Put your changes here
+\treturn uValue
+
+[PreValidate]
+method PreValidate() class %FORM:NAME%
+\tsuper:PreValidate()
+\t//Put your changes here
+\treturn NIL
+
+[QueryClose]
+method QueryClose(oEvent) class %FORM:NAME%
+\tlocal lAllowClose as logic
+\tlAllowClose := super:QueryClose(oEvent)
+\t//Put your changes here
+\treturn lAllowClose
+
+[Resize]
+method Resize(oResizeEvent) class %FORM:NAME%
+\tsuper:Resize(oResizeEvent)
+\t//Put your changes here
+\treturn NIL
+
+[VerticalScroll]
+method VerticalScroll(oScrollEvent) class %FORM:NAME%
+\tsuper:VerticalScroll(oScrollEvent)
+\t//Put your changes here
+\treturn NIL
+
+[HorizontalSlide]
+method HorizontalSlide(oSlideEvent) class %FORM:NAME%
+\tsuper:HorizontalSlide(oSlideEvent)
+\t//Put your changes here
+\treturn NIL
+
+[VerticalSlide]
+method VerticalSlide(oSlideEvent) class %FORM:NAME%
+\tsuper:VerticalSlide(oSlideEvent)
+\t//Put your changes here
+\treturn NIL
+
+[HorizontalSpin]
+method HorizontalSpin(oSpinEvent) class %FORM:NAME%
+\tsuper:HorizontalSpin(oSpinEvent)
+\t//Put your changes here
+\treturn NIL
+
+[VerticalSpin]
+method VerticalSpin(oSpinEvent) class %FORM:NAME%
+\tsuper:VerticalSpin(oSpinEvent)
+\t//Put your changes here
+\treturn NIL
+
+[AnimationStart]
+method AnimationStart(oControlNotifyEvent) class %FORM:NAME%
+\tlocal oControl as Control
+\toControl := IIf(oControlNotifyEvent == NULL_OBJECT, NULL_OBJECT, oControlNotifyEvent:Control)
+\tsuper:AnimationStart(oControlNotifyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[AnimationStop]
+method AnimationStop(oControlNotifyEvent) class %FORM:NAME%
+\tlocal oControl as Control
+\toControl := IIf(oControlNotifyEvent == NULL_OBJECT, NULL_OBJECT, oControlNotifyEvent:Control)
+\tsuper:AnimationStop(oControlNotifyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[RichEditDropFiles]
+method RichEditDropFiles(oRichEditDropEvent) class %FORM:NAME%
+\tsuper:RichEditDropFiles(oRichEditDropEvent)
+\t//Put your changes here
+\treturn NIL
+
+[RichEditProtected]
+method RichEditProtected(oRichEditProtectEvent) class %FORM:NAME%
+\tsuper:RichEditProtected(oRichEditProtectEvent)
+\t//Put your changes here
+\treturn NIL
+
+[RichEditSelectionChange]
+method RichEditSelectionChange(oRichEditSelectionEvent) class %FORM:NAME%
+\tsuper:RichEditSelectionChange(oRichEditSelectionEvent)
+\t//Put your changes here
+\treturn NIL
+
+[RichEditUndoLost]
+method RichEditUndoLost(oControlNotifyEvent) class %FORM:NAME%
+\tsuper:RichEditUndoLost(oControlNotifyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ListViewItemDrag]
+method ListViewItemDrag(oListViewDragEvent) class %FORM:NAME%
+\tsuper:ListViewItemDrag(oListViewDragEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ListViewItemEdit]
+method ListViewItemEdit(oListViewEditEvent) class %FORM:NAME%
+\tsuper:ListViewItemEdit(oListViewEditEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ListViewColumnClick]
+method ListViewColumnClick(oListViewColumnClickEvent) class %FORM:NAME%
+\tsuper:ListViewColumnClick(oListViewColumnClickEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ListViewItemDelete]
+method ListViewItemDelete(oListViewDeleteEvent) class %FORM:NAME%
+\tsuper:ListViewItemDelete(oListViewDeleteEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ListViewKeyDown]
+method ListViewKeyDown(oListViewKeyEvent) class %FORM:NAME%
+\tsuper:ListViewKeyDown(oListViewKeyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewMouseButtonDown]
+method TreeViewMouseButtonDown(oTreeViewMouseEvent) class %FORM:NAME%
+\tsuper:TreeViewMouseButtonDown(oTreeViewMouseEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ListViewMouseButtonDown]
+method ListViewMouseButtonDown(oListViewMouseEvent) class %FORM:NAME%
+\tsuper:ListViewMouseButtonDown(oListViewMouseEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewMouseButtonDoubleClick]
+method TreeViewMouseButtonDoubleClick(oTreeViewMouseEvent) class %FORM:NAME%
+\tsuper:TreeViewMouseButtonDoubleClick(oTreeViewMouseEvent)
+\t//Put your changes here
+\treturn NIL
+
+[ListViewMouseButtonDoubleClick]
+method ListViewMouseButtonDoubleClick(oListViewMouseEvent) class %FORM:NAME%
+\tsuper:ListViewMouseButtonDoubleClick(oListViewMouseEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TabSelect]
+method TabSelect(oControlNotifyEvent) class %FORM:NAME%
+\tsuper:TabSelect(oControlNotifyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TabSelectionChanging]
+method TabSelectionChanging(oControlNotifyEvent) class %FORM:NAME%
+\tsuper:TabSelectionChanging(oControlNotifyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TabKeyDown]
+method TabKeyDown(oControlNotifyEvent) class %FORM:NAME%
+\tsuper:TabKeyDown(oControlNotifyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewItemDrag]
+method TreeViewItemDrag(oTreeViewDragEvent) class %FORM:NAME%
+\tsuper:TreeViewItemDrag(oTreeViewDragEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewItemEdit]
+method TreeViewItemEdit(oTreeViewEditEvent) class %FORM:NAME%
+\tsuper:TreeViewItemEdit(oTreeViewEditEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewItemDelete]
+method TreeViewItemDelete(oTreeViewDeleteEvent) class %FORM:NAME%
+\tsuper:TreeViewItemDelete(oTreeViewDeleteEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewItemExpanded]
+method TreeViewItemExpanded(oTreeViewExpandedEvent) class %FORM:NAME%
+\tsuper:TreeViewItemExpanded(oTreeViewExpandedEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewItemExpanding]
+method TreeViewItemExpanding(oTreeViewExpandingEvent) class %FORM:NAME%
+\tsuper:TreeViewItemExpanding(oTreeViewExpandingEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewKeyDown]
+method TreeViewKeyDown(oTreeViewKeyEvent) class %FORM:NAME%
+\tsuper:TreeViewKeyDown(oTreeViewKeyEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewSelectionChanged]
+method TreeViewSelectionChanged(oTreeViewSelectionEvent) class %FORM:NAME%
+\tsuper:TreeViewSelectionChanged(oTreeViewSelectionEvent)
+\t//Put your changes here
+\treturn NIL
+
+[TreeViewSelectionChanging]
+method TreeViewSelectionChanging(oTreeViewSelectionEvent) class %FORM:NAME%
+\tsuper:TreeViewSelectionChanging(oTreeViewSelectionEvent)
+\t//Put your changes here
+\treturn NIL
+
+[MonthCalSelectionChanged]
+method MonthCalSelectionChanged(oMonthCalSelectionEvent) class %FORM:NAME%
+\tsuper:MonthCalSelectionChanged(oMonthCalSelectionEvent)
+\t//Put your changes here
+\treturn NIL
+
+[DateTimeSelectionChanged]
+method DateTimeSelectionChanged(oDateTimeSelectionEvent) class %FORM:NAME%
+\tsuper:DateTimeSelectionChanged(oDateTimeSelectionEvent)
+\t//Put your changes here
+\treturn NIL
+
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Resources/HelpAbt.BMP b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Resources/HelpAbt.BMP
new file mode 100644
index 0000000000..63825a67f1
Binary files /dev/null and b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Resources/HelpAbt.BMP differ
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Resources/XSharp.ICO b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Resources/XSharp.ICO
new file mode 100644
index 0000000000..dd1707e251
Binary files /dev/null and b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Resources/XSharp.ICO differ
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Resources/cctl6.man b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Resources/cctl6.man
new file mode 100644
index 0000000000..184d3bffa9
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Resources/cctl6.man
@@ -0,0 +1,23 @@
+
+
+
+Visual Objects Application.
+
+
+
+
+
+
+
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.EmptySDIMenu.rc b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.EmptySDIMenu.rc
new file mode 100644
index 0000000000..73b5c61d08
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.EmptySDIMenu.rc
@@ -0,0 +1,40 @@
+
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime version: 4.0.30319.42000
+// Generator : XSharp.MenuEditor 2.1.0.0
+// Timestamp : 28-1-2020 08:22:38
+//
+// Changes to this file may cause incorrect behavior and may be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#define IDM_EmptySDIMenu_File_ID 18500
+#define IDM_EmptySDIMenu_File_Open_ID 18501
+#define IDM_EmptySDIMenu_File_Print_Setup_ID 18503
+#define IDM_EmptySDIMenu_File_Exit_ID 18505
+#define IDM_EmptySDIMenu_Help_ID 18506
+#define IDM_EmptySDIMenu_Help_Index_ID 18507
+#define IDM_EmptySDIMenu_Help_Using_Help_ID 18508
+#define IDM_EmptySDIMenu_Help_About_ID 18510
+EmptySDIMenu MENU
+BEGIN
+ POPUP "&File"
+ BEGIN
+ MENUITEM "&Open...\tCtrl+O" , IDM_EmptySDIMenu_File_Open_ID
+ MENUITEM SEPARATOR
+ MENUITEM "P&rint Setup..." , IDM_EmptySDIMenu_File_Print_Setup_ID
+ MENUITEM SEPARATOR
+ MENUITEM "E&xit\tAlt+F4" , IDM_EmptySDIMenu_File_Exit_ID
+ END
+ POPUP "&Help"
+ BEGIN
+ MENUITEM "&Index\tF1" , IDM_EmptySDIMenu_Help_Index_ID
+ MENUITEM "&Using Help" , IDM_EmptySDIMenu_Help_Using_Help_ID
+ MENUITEM SEPARATOR
+ MENUITEM "&About..." , IDM_EmptySDIMenu_Help_About_ID
+ END
+END
+
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.EmptySDIMenu.xsmnu b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.EmptySDIMenu.xsmnu
new file mode 100644
index 0000000000..0b595b3464
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.EmptySDIMenu.xsmnu
@@ -0,0 +1,226 @@
+
+
+
+ EmptySDIMenu
+
+
+
+
+ Flat Toolbar
+ Icon
+ No
+ 18500
+
+
+
+ Help
+ &Help
+
+
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ HelpIndex
+ &Index
+ Index of help
+ Help_Index
+ F1
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ HelpUsingHelp
+ &Using Help
+ How to use help
+ Help_UsingHelp
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+
+
+ -------------------------
+
+
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ HelpAbout
+ &About...
+ About application
+
+
+ None
+ Yes
+ No
+ Help
+
+
+ Help About
+ 2
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.EmptySDIMenu_Accelerator.rc b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.EmptySDIMenu_Accelerator.rc
new file mode 100644
index 0000000000..35f20c8812
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.EmptySDIMenu_Accelerator.rc
@@ -0,0 +1,27 @@
+
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime version: 4.0.30319.42000
+// Generator : XSharp.MenuEditor 2.1.0.0
+// Timestamp : 28-1-2020 08:22:38
+//
+// Changes to this file may cause incorrect behavior and may be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#define IDM_EmptySDIMenu_File_ID 18500
+#define IDM_EmptySDIMenu_File_Open_ID 18501
+#define IDM_EmptySDIMenu_File_Print_Setup_ID 18503
+#define IDM_EmptySDIMenu_File_Exit_ID 18505
+#define IDM_EmptySDIMenu_Help_ID 18506
+#define IDM_EmptySDIMenu_Help_Index_ID 18507
+#define IDM_EmptySDIMenu_Help_Using_Help_ID 18508
+#define IDM_EmptySDIMenu_Help_About_ID 18510
+EmptySDIMenu_Accelerator ACCELERATORS
+BEGIN
+ 79, IDM_EmptySDIMenu_File_Open_ID, CONTROL ,VIRTKEY
+ 115, IDM_EmptySDIMenu_File_Exit_ID, ALT ,VIRTKEY
+ 112, IDM_EmptySDIMenu_Help_Index_ID, VIRTKEY
+END
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.StandardSDIMenu.rc b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.StandardSDIMenu.rc
new file mode 100644
index 0000000000..dc70afa2d1
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.StandardSDIMenu.rc
@@ -0,0 +1,76 @@
+
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime version: 4.0.30319.42000
+// Generator : XSharp.MenuEditor 2.1.0.0
+// Timestamp : 28-1-2020 08:22:41
+//
+// Changes to this file may cause incorrect behavior and may be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#define IDM_StandardSDIMenu_File_ID 29000
+#define IDM_StandardSDIMenu_File_Open_ID 29001
+#define IDM_StandardSDIMenu_File_Print_ID 29003
+#define IDM_StandardSDIMenu_File_Print_Setup_ID 29004
+#define IDM_StandardSDIMenu_File_Exit_ID 29006
+#define IDM_StandardSDIMenu_Edit_ID 29007
+#define IDM_StandardSDIMenu_Edit_Cut_ID 29008
+#define IDM_StandardSDIMenu_Edit_Copy_ID 29009
+#define IDM_StandardSDIMenu_Edit_Paste_ID 29010
+#define IDM_StandardSDIMenu_Edit_Insert_Record_ID 29012
+#define IDM_StandardSDIMenu_Edit_Delete_Record_ID 29013
+#define IDM_StandardSDIMenu_Edit_Go_To_Top_ID 29015
+#define IDM_StandardSDIMenu_Edit_Previous_ID 29016
+#define IDM_StandardSDIMenu_Edit_Next_ID 29017
+#define IDM_StandardSDIMenu_Edit_Go_To_Bottom_ID 29018
+#define IDM_StandardSDIMenu_View_ID 29019
+#define IDM_StandardSDIMenu_View_Form_ID 29020
+#define IDM_StandardSDIMenu_View_Table_ID 29021
+#define IDM_StandardSDIMenu_Help_ID 29022
+#define IDM_StandardSDIMenu_Help_Index_ID 29023
+#define IDM_StandardSDIMenu_Help_Context_Help_ID 29024
+#define IDM_StandardSDIMenu_Help_Using_Help_ID 29025
+#define IDM_StandardSDIMenu_Help_About_ID 29027
+StandardSDIMenu MENU
+BEGIN
+ POPUP "&File"
+ BEGIN
+ MENUITEM "&Open...\tCtrl+O" , IDM_StandardSDIMenu_File_Open_ID
+ MENUITEM SEPARATOR
+ MENUITEM "&Print" , IDM_StandardSDIMenu_File_Print_ID
+ MENUITEM "P&rint Setup..." , IDM_StandardSDIMenu_File_Print_Setup_ID
+ MENUITEM SEPARATOR
+ MENUITEM "E&xit\tAlt+F4" , IDM_StandardSDIMenu_File_Exit_ID
+ END
+ POPUP "&Edit"
+ BEGIN
+ MENUITEM "Cu&t\tCtrl+X" , IDM_StandardSDIMenu_Edit_Cut_ID
+ MENUITEM "&Copy\tCtrl+C" , IDM_StandardSDIMenu_Edit_Copy_ID
+ MENUITEM "&Paste\tCtrl+V" , IDM_StandardSDIMenu_Edit_Paste_ID
+ MENUITEM SEPARATOR
+ MENUITEM "&Insert Record" , IDM_StandardSDIMenu_Edit_Insert_Record_ID
+ MENUITEM "&Delete Record" , IDM_StandardSDIMenu_Edit_Delete_Record_ID
+ MENUITEM SEPARATOR
+ MENUITEM "G&o To Top\tCtrl+Home" , IDM_StandardSDIMenu_Edit_Go_To_Top_ID
+ MENUITEM "Pre&vious" , IDM_StandardSDIMenu_Edit_Previous_ID
+ MENUITEM "&Next" , IDM_StandardSDIMenu_Edit_Next_ID
+ MENUITEM "Go To &Bottom\tCtrl+End" , IDM_StandardSDIMenu_Edit_Go_To_Bottom_ID
+ END
+ POPUP "&View"
+ BEGIN
+ MENUITEM "&Form\tShift+F2" , IDM_StandardSDIMenu_View_Form_ID
+ MENUITEM "&Table\tF2" , IDM_StandardSDIMenu_View_Table_ID
+ END
+ POPUP "&Help"
+ BEGIN
+ MENUITEM "&Index\tF1" , IDM_StandardSDIMenu_Help_Index_ID
+ MENUITEM "&Context Help\tCtrl+F1" , IDM_StandardSDIMenu_Help_Context_Help_ID
+ MENUITEM "&Using Help" , IDM_StandardSDIMenu_Help_Using_Help_ID
+ MENUITEM SEPARATOR
+ MENUITEM "&About..." , IDM_StandardSDIMenu_Help_About_ID
+ END
+END
+
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.StandardSDIMenu.xsmnu b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.StandardSDIMenu.xsmnu
new file mode 100644
index 0000000000..5087ad497b
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.StandardSDIMenu.xsmnu
@@ -0,0 +1,515 @@
+
+
+
+ StandardSDIMenu
+
+
+
+
+ Flat Toolbar
+ Icon
+ No
+ 29000
+
+
+ File
+ &File
+
+
+ File
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ FileOpen
+ &Open...
+ Open a file
+ File_Open
+ Ctrl+O
+ Yes
+ No
+ Open
+ Open
+ Open File
+ 1
+ IDM_FILE_OPEN
+
+
+
+
+ -------------------------
+
+
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ FilePrint
+ &Print
+ Print the window
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ FilePrinterSetup
+ P&rint Setup...
+ Setup printer options
+ File_Printer_Setup
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+
+
+ -------------------------
+
+
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ FileExit
+ E&xit
+ End of application
+ File_Exit
+ Alt+F4
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+
+
+ Edit
+ &Edit
+ Edit information
+ Edit
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ Cut
+ Cu&t
+ Cut to clipboard
+ Cut
+ Ctrl+X
+ Yes
+ No
+ Cut
+ Cut
+ Cut
+ 3
+
+
+
+
+ Copy
+ &Copy
+ Copy to clipboard
+ Copy
+ Ctrl+C
+ Yes
+ No
+ Copy
+ Copy
+ Copy
+ 4
+
+
+
+
+ Paste
+ &Paste
+ Paste from clipboard
+ Paste
+ Ctrl+V
+ Yes
+ No
+ Paste
+ Paste
+ Paste
+ 5
+
+
+
+
+
+
+ -------------------------
+
+
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ Append
+ &Insert Record
+ Insert a new record
+ Append
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ Delete
+ &Delete Record
+ Delete this record
+ Delete
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+
+
+ -------------------------
+
+
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ GoTop
+ G&o To Top
+ Go to first record
+ GoTop
+ Ctrl+Home
+ Yes
+ No
+ Start Record
+ Go Top
+ Go Top
+ 7
+
+
+
+
+ SkipPrevious
+ Pre&vious
+ Go to previous record
+ SkipPrevious
+ None
+ Yes
+ No
+ Previous Record
+ Previous Record
+ Previous Record
+ 8
+
+
+
+
+ SkipNext
+ &Next
+ Go to next record
+ SkipNext
+ None
+ Yes
+ No
+ Next Record
+ Next Record
+ Next Record
+ 9
+
+
+
+
+ GoBottom
+ Go To &Bottom
+ Go to last record
+ GoBottom
+ Ctrl+End
+ Yes
+ No
+ End Record
+ Go Bottom
+ Go Bottom
+ 10
+
+
+
+
+
+
+ View
+ &View
+
+
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ ViewForm
+ &Form
+ View a single record as a form
+ ViewForm
+ Shift+F2
+ Yes
+ No
+ View Form
+ Form View
+ Form View
+ 12
+
+
+
+
+ ViewTable
+ &Table
+ View several records in a table
+ ViewTable
+ F2
+ Yes
+ No
+ View Table
+ Browse View
+ Browse View
+ 13
+
+
+
+
+
+
+ Help
+ &Help
+
+
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ HelpIndex
+ &Index
+ Index of help
+ Help_Index
+ F1
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ HelpContext
+ &Context Help
+ Context sensitive help
+ Help_ContextHelp
+ Ctrl+F1
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ HelpUsingHelp
+ &Using Help
+ How to use help
+ Help_UsingHelp
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+
+
+ -------------------------
+
+
+
+
+ None
+ Yes
+ No
+
+
+
+
+
+
+ -1
+
+
+
+
+ HelpAbout
+ &About...
+ About application
+
+
+ None
+ Yes
+ No
+ Help
+ Help
+ Help
+ 15
+ IDM_HELP_ABOUT
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.StandardSDIMenu_Accelerator.rc b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.StandardSDIMenu_Accelerator.rc
new file mode 100644
index 0000000000..aa9c190f62
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.StandardSDIMenu_Accelerator.rc
@@ -0,0 +1,50 @@
+
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime version: 4.0.30319.42000
+// Generator : XSharp.MenuEditor 2.1.0.0
+// Timestamp : 28-1-2020 08:22:41
+//
+// Changes to this file may cause incorrect behavior and may be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#define IDM_StandardSDIMenu_File_ID 29000
+#define IDM_StandardSDIMenu_File_Open_ID 29001
+#define IDM_StandardSDIMenu_File_Print_ID 29003
+#define IDM_StandardSDIMenu_File_Print_Setup_ID 29004
+#define IDM_StandardSDIMenu_File_Exit_ID 29006
+#define IDM_StandardSDIMenu_Edit_ID 29007
+#define IDM_StandardSDIMenu_Edit_Cut_ID 29008
+#define IDM_StandardSDIMenu_Edit_Copy_ID 29009
+#define IDM_StandardSDIMenu_Edit_Paste_ID 29010
+#define IDM_StandardSDIMenu_Edit_Insert_Record_ID 29012
+#define IDM_StandardSDIMenu_Edit_Delete_Record_ID 29013
+#define IDM_StandardSDIMenu_Edit_Go_To_Top_ID 29015
+#define IDM_StandardSDIMenu_Edit_Previous_ID 29016
+#define IDM_StandardSDIMenu_Edit_Next_ID 29017
+#define IDM_StandardSDIMenu_Edit_Go_To_Bottom_ID 29018
+#define IDM_StandardSDIMenu_View_ID 29019
+#define IDM_StandardSDIMenu_View_Form_ID 29020
+#define IDM_StandardSDIMenu_View_Table_ID 29021
+#define IDM_StandardSDIMenu_Help_ID 29022
+#define IDM_StandardSDIMenu_Help_Index_ID 29023
+#define IDM_StandardSDIMenu_Help_Context_Help_ID 29024
+#define IDM_StandardSDIMenu_Help_Using_Help_ID 29025
+#define IDM_StandardSDIMenu_Help_About_ID 29027
+StandardSDIMenu_Accelerator ACCELERATORS
+BEGIN
+ 79, IDM_StandardSDIMenu_File_Open_ID, CONTROL ,VIRTKEY
+ 115, IDM_StandardSDIMenu_File_Exit_ID, ALT ,VIRTKEY
+ 88, IDM_StandardSDIMenu_Edit_Cut_ID, CONTROL ,VIRTKEY
+ 67, IDM_StandardSDIMenu_Edit_Copy_ID, CONTROL ,VIRTKEY
+ 86, IDM_StandardSDIMenu_Edit_Paste_ID, CONTROL ,VIRTKEY
+ 36, IDM_StandardSDIMenu_Edit_Go_To_Top_ID, CONTROL ,VIRTKEY
+ 35, IDM_StandardSDIMenu_Edit_Go_To_Bottom_ID, CONTROL ,VIRTKEY
+ 113, IDM_StandardSDIMenu_View_Form_ID, SHIFT ,VIRTKEY
+ 113, IDM_StandardSDIMenu_View_Table_ID, VIRTKEY
+ 112, IDM_StandardSDIMenu_Help_Index_ID, VIRTKEY
+ 112, IDM_StandardSDIMenu_Help_Context_Help_ID, CONTROL ,VIRTKEY
+END
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.VS_VERSION_INFO.rc b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.VS_VERSION_INFO.rc
new file mode 100644
index 0000000000..28e0038020
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.VS_VERSION_INFO.rc
@@ -0,0 +1,31 @@
+VS_VERSION_INFO VERSIONINFO
+FILEVERSION 1, 0, 0, 0
+PRODUCTVERSION 1, 0, 0, 0
+FILEFLAGSMASK 0x3fL
+FILEFLAGS 0x00L
+FILEOS VOS__WINDOWS32
+FILETYPE VFT_APP
+FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0"
+ BEGIN
+ VALUE "Comments", "This is a VOTestBed!\0"
+ VALUE "CompanyName", "XSharp BV\0"
+ VALUE "FileVersion", "1, 0, 0, 0\0"
+ VALUE "FileDescription", "X# VOTestBed\0"
+ VALUE "ProductVersion", "1.0.0.0\0"
+ VALUE "InternalName", "VO MDI App\0"
+ VALUE "ProductName", "X# VOTestBed\0"
+ VALUE "LegalCopyright", "Copyright \251 2015-2022\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200
+ END
+END
+
+
+
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.prg b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.prg
new file mode 100644
index 0000000000..f884a28c27
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/SDI Menus.prg
@@ -0,0 +1,240 @@
+#region DEFINES
+DEFINE IDM_StandardSDIMenu_File_ID := 29000
+DEFINE IDM_StandardSDIMenu_File_Open_ID := 29001
+DEFINE IDM_StandardSDIMenu_File_Print_ID := 29003
+DEFINE IDM_StandardSDIMenu_File_Print_Setup_ID := 29004
+DEFINE IDM_StandardSDIMenu_File_Exit_ID := 29006
+DEFINE IDM_StandardSDIMenu_Edit_ID := 29007
+DEFINE IDM_StandardSDIMenu_Edit_Cut_ID := 29008
+DEFINE IDM_StandardSDIMenu_Edit_Copy_ID := 29009
+DEFINE IDM_StandardSDIMenu_Edit_Paste_ID := 29010
+DEFINE IDM_StandardSDIMenu_Edit_Insert_Record_ID := 29012
+DEFINE IDM_StandardSDIMenu_Edit_Delete_Record_ID := 29013
+DEFINE IDM_StandardSDIMenu_Edit_Go_To_Top_ID := 29015
+DEFINE IDM_StandardSDIMenu_Edit_Previous_ID := 29016
+DEFINE IDM_StandardSDIMenu_Edit_Next_ID := 29017
+DEFINE IDM_StandardSDIMenu_Edit_Go_To_Bottom_ID := 29018
+DEFINE IDM_StandardSDIMenu_View_ID := 29019
+DEFINE IDM_StandardSDIMenu_View_Form_ID := 29020
+DEFINE IDM_StandardSDIMenu_View_Table_ID := 29021
+DEFINE IDM_StandardSDIMenu_Help_ID := 29022
+DEFINE IDM_StandardSDIMenu_Help_Index_ID := 29023
+DEFINE IDM_StandardSDIMenu_Help_Context_Help_ID := 29024
+DEFINE IDM_StandardSDIMenu_Help_Using_Help_ID := 29025
+DEFINE IDM_StandardSDIMenu_Help_About_ID := 29027
+DEFINE IDM_EmptySDIMenu_File_ID := 18500
+DEFINE IDM_EmptySDIMenu_File_Open_ID := 18501
+DEFINE IDM_EmptySDIMenu_File_Print_Setup_ID := 18503
+DEFINE IDM_EmptySDIMenu_File_Exit_ID := 18505
+DEFINE IDM_EmptySDIMenu_Help_ID := 18506
+DEFINE IDM_EmptySDIMenu_Help_Index_ID := 18507
+DEFINE IDM_EmptySDIMenu_Help_Using_Help_ID := 18508
+DEFINE IDM_EmptySDIMenu_Help_About_ID := 18510
+#endregion
+
+PARTIAL CLASS EmptySDIMenu INHERIT Menu
+
+CONSTRUCTOR( oOwner )
+
+ LOCAL oTB AS Toolbar
+
+ SELF:PreInit()
+
+ SUPER( ResourceID { "EmptySDIMenu" , _GetInst( ) } )
+
+ SELF:RegisterItem(IDM_EmptySDIMenu_File_ID, ;
+ HyperLabel{ #File , "&File" , , "File" } , SELF:Handle() , 0)
+
+ SELF:RegisterItem(IDM_EmptySDIMenu_File_Open_ID, ;
+ HyperLabel{ #FileOpen , "&Open... Ctrl+O" , "Open a file" , "File_Open" })
+
+ SELF:RegisterItem(IDM_EmptySDIMenu_File_Print_Setup_ID, ;
+ HyperLabel{ #FilePrinterSetup , "P&rint Setup..." , "Setup printer options" , "File_Printer_Setup" })
+
+ SELF:RegisterItem(IDM_EmptySDIMenu_File_Exit_ID, ;
+ HyperLabel{ #FileExit , "E&xit Alt+F4" , "End of application" , "File_Exit" })
+
+ SELF:RegisterItem(IDM_EmptySDIMenu_Help_ID, ;
+ HyperLabel{ #Help , "&Help" , , } , SELF:Handle() , 1)
+
+ SELF:RegisterItem(IDM_EmptySDIMenu_Help_Index_ID, ;
+ HyperLabel{ #HelpIndex , "&Index F1" , "Index of help" , "Help_Index" })
+
+ SELF:RegisterItem(IDM_EmptySDIMenu_Help_Using_Help_ID, ;
+ HyperLabel{ #HelpUsingHelp , "&Using Help" , "How to use help" , "Help_UsingHelp" })
+
+ SELF:RegisterItem(IDM_EmptySDIMenu_Help_About_ID, ;
+ HyperLabel{ #HelpAbout , "&About..." , "About application" , })
+
+ oTB := Toolbar{}
+
+ oTB:ButtonStyle := TB_ICONONLY
+ oTB:Flat := TRUE
+ oTB:EnableBands(FALSE)
+
+ oTB:AppendItem(IDT_OPEN , IDM_EmptySDIMenu_File_Open_ID)
+ oTB:AddTipText(IDT_OPEN , IDM_EmptySDIMenu_File_Open_ID , "Open File")
+
+ oTB:AppendItem(IDT_HELP , IDM_EmptySDIMenu_Help_About_ID)
+ oTB:AddTipText(IDT_HELP , IDM_EmptySDIMenu_Help_About_ID , "Help About")
+
+
+ SELF:ToolBar := oTB
+ SELF:Accelerator := EmptySDIMenu_Accelerator{ }
+
+ SELF:PostInit()
+
+ RETURN
+
+END CLASS
+PARTIAL CLASS EmptySDIMenu_Accelerator INHERIT Accelerator
+
+CONSTRUCTOR()
+ SUPER( ResourceID { "EmptySDIMenu_Accelerator" , _GetInst( ) } )
+RETURN
+
+
+END CLASS
+PARTIAL CLASS StandardSDIMenu INHERIT Menu
+
+CONSTRUCTOR( oOwner )
+
+ LOCAL oTB AS Toolbar
+
+ SELF:PreInit()
+
+ SUPER( ResourceID { "StandardSDIMenu" , _GetInst( ) } )
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_File_ID, ;
+ HyperLabel{ #File , "&File" , , "File" } , SELF:Handle() , 0)
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_File_Open_ID, ;
+ HyperLabel{ #FileOpen , "&Open... Ctrl+O" , "Open a file" , "File_Open" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_File_Print_ID, ;
+ HyperLabel{ #FilePrint , "&Print" , "Print the window" , })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_File_Print_Setup_ID, ;
+ HyperLabel{ #FilePrinterSetup , "P&rint Setup..." , "Setup printer options" , "File_Printer_Setup" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_File_Exit_ID, ;
+ HyperLabel{ #FileExit , "E&xit Alt+F4" , "End of application" , "File_Exit" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_ID, ;
+ HyperLabel{ #Edit , "&Edit" , "Edit information" , "Edit" } , SELF:Handle() , 1)
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_Cut_ID, ;
+ HyperLabel{ #Cut , "Cu&t Ctrl+X" , "Cut to clipboard" , "Cut" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_Copy_ID, ;
+ HyperLabel{ #Copy , "&Copy Ctrl+C" , "Copy to clipboard" , "Copy" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_Paste_ID, ;
+ HyperLabel{ #Paste , "&Paste Ctrl+V" , "Paste from clipboard" , "Paste" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_Insert_Record_ID, ;
+ HyperLabel{ #Append , "&Insert Record" , "Insert a new record" , "Append" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_Delete_Record_ID, ;
+ HyperLabel{ #Delete , "&Delete Record" , "Delete this record" , "Delete" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_Go_To_Top_ID, ;
+ HyperLabel{ #GoTop , "G&o To Top Ctrl+Home" , "Go to first record" , "GoTop" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_Previous_ID, ;
+ HyperLabel{ #SkipPrevious , "Pre&vious" , "Go to previous record" , "SkipPrevious" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_Next_ID, ;
+ HyperLabel{ #SkipNext , "&Next" , "Go to next record" , "SkipNext" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Edit_Go_To_Bottom_ID, ;
+ HyperLabel{ #GoBottom , "Go To &Bottom Ctrl+End" , "Go to last record" , "GoBottom" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_View_ID, ;
+ HyperLabel{ #View , "&View" , , } , SELF:Handle() , 2)
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_View_Form_ID, ;
+ HyperLabel{ #ViewForm , "&Form Shift+F2" , "View a single record as a form" , "ViewForm" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_View_Table_ID, ;
+ HyperLabel{ #ViewTable , "&Table F2" , "View several records in a table" , "ViewTable" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Help_ID, ;
+ HyperLabel{ #Help , "&Help" , , } , SELF:Handle() , 3)
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Help_Index_ID, ;
+ HyperLabel{ #HelpIndex , "&Index F1" , "Index of help" , "Help_Index" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Help_Context_Help_ID, ;
+ HyperLabel{ #HelpContext , "&Context Help Ctrl+F1" , "Context sensitive help" , "Help_ContextHelp" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Help_Using_Help_ID, ;
+ HyperLabel{ #HelpUsingHelp , "&Using Help" , "How to use help" , "Help_UsingHelp" })
+
+ SELF:RegisterItem(IDM_StandardSDIMenu_Help_About_ID, ;
+ HyperLabel{ #HelpAbout , "&About..." , "About application" , })
+
+ oTB := Toolbar{}
+
+ oTB:ButtonStyle := TB_ICONONLY
+ oTB:Flat := TRUE
+ oTB:EnableBands(FALSE)
+
+ oTB:AppendItem(IDT_OPEN , IDM_StandardSDIMenu_File_Open_ID)
+ oTB:AddTipText(IDT_OPEN , IDM_StandardSDIMenu_File_Open_ID , "Open File")
+
+ oTB:AppendItem(IDT_SEPARATOR)
+
+ oTB:AppendItem(IDT_CUT , IDM_StandardSDIMenu_Edit_Cut_ID)
+ oTB:AddTipText(IDT_CUT , IDM_StandardSDIMenu_Edit_Cut_ID , "Cut")
+
+ oTB:AppendItem(IDT_COPY , IDM_StandardSDIMenu_Edit_Copy_ID)
+ oTB:AddTipText(IDT_COPY , IDM_StandardSDIMenu_Edit_Copy_ID , "Copy")
+
+ oTB:AppendItem(IDT_PASTE , IDM_StandardSDIMenu_Edit_Paste_ID)
+ oTB:AddTipText(IDT_PASTE , IDM_StandardSDIMenu_Edit_Paste_ID , "Paste")
+
+ oTB:AppendItem(IDT_SEPARATOR)
+
+ oTB:AppendItem(IDT_STARTREC , IDM_StandardSDIMenu_Edit_Go_To_Top_ID)
+ oTB:AddTipText(IDT_STARTREC , IDM_StandardSDIMenu_Edit_Go_To_Top_ID , "Go Top")
+
+ oTB:AppendItem(IDT_PREVREC , IDM_StandardSDIMenu_Edit_Previous_ID)
+ oTB:AddTipText(IDT_PREVREC , IDM_StandardSDIMenu_Edit_Previous_ID , "Previous Record")
+
+ oTB:AppendItem(IDT_NEXTREC , IDM_StandardSDIMenu_Edit_Next_ID)
+ oTB:AddTipText(IDT_NEXTREC , IDM_StandardSDIMenu_Edit_Next_ID , "Next Record")
+
+ oTB:AppendItem(IDT_ENDREC , IDM_StandardSDIMenu_Edit_Go_To_Bottom_ID)
+ oTB:AddTipText(IDT_ENDREC , IDM_StandardSDIMenu_Edit_Go_To_Bottom_ID , "Go Bottom")
+
+ oTB:AppendItem(IDT_SEPARATOR)
+
+ oTB:AppendItem(IDT_VFORM , IDM_StandardSDIMenu_View_Form_ID)
+ oTB:AddTipText(IDT_VFORM , IDM_StandardSDIMenu_View_Form_ID , "Form View")
+
+ oTB:AppendItem(IDT_VGBROWSE , IDM_StandardSDIMenu_View_Table_ID)
+ oTB:AddTipText(IDT_VGBROWSE , IDM_StandardSDIMenu_View_Table_ID , "Browse View")
+
+ oTB:AppendItem(IDT_SEPARATOR)
+
+ oTB:AppendItem(IDT_HELP , IDM_StandardSDIMenu_Help_About_ID)
+ oTB:AddTipText(IDT_HELP , IDM_StandardSDIMenu_Help_About_ID , "Help")
+
+
+ SELF:ToolBar := oTB
+ SELF:Accelerator := StandardSDIMenu_Accelerator{ }
+
+ SELF:PostInit()
+
+ RETURN
+
+END CLASS
+PARTIAL CLASS StandardSDIMenu_Accelerator INHERIT Accelerator
+
+CONSTRUCTOR()
+ SUPER( ResourceID { "StandardSDIMenu_Accelerator" , _GetInst( ) } )
+RETURN
+
+
+END CLASS
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Standard SDI.IDI_STANDARDICON.rc b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Standard SDI.IDI_STANDARDICON.rc
new file mode 100644
index 0000000000..aa3dfe446e
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Standard SDI.IDI_STANDARDICON.rc
@@ -0,0 +1 @@
+ 101 ICON "Resources\\XSharp.ICO"
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Standard SDI.Manifest.rc b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Standard SDI.Manifest.rc
new file mode 100644
index 0000000000..c95c0e84da
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Standard SDI.Manifest.rc
@@ -0,0 +1,4 @@
+ 1 24 resources\cctl6.man
+
+
+
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Standard SDI.prg b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Standard SDI.prg
new file mode 100644
index 0000000000..e9912c77b6
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Standard SDI.prg
@@ -0,0 +1,118 @@
+#region DEFINES
+define IDI_STANDARDICON := 101
+define IDI_VOICON := 102
+#endregion
+
+class StandardSDIWindow inherit DataWindow
+ protect oPrinter as PrintingDevice
+ protect oStdMenu as Menu
+
+
+METHOD DoOpenFile(cFileName, lReadOnly)
+ LOCAL oTB AS TextBox
+
+ IF (Len(cFileName) > 3) .AND. (Upper(Right(cFileName, 4)) == ".DBF")
+ IF (SELF:Menu != oStdMenu)
+ SELF:Menu := oStdMenu
+ SELF:ToolBar:PressItem(IDM_StandardSDIMenu_View_Form_ID, #MenuItemID)
+ ENDIF
+
+ SELF:Use(DBServer{cFileName, , lReadOnly})
+ SELF:Caption := "Browse Database: " + cFileName
+ ELSE
+ oTB := TextBox{SELF, "File Open Error", "Cannot open " + cFileName + " - Not a DBF file"}
+ oTB:Type := BUTTONOKAY
+ oTB:Beep:=TRUE
+ oTB:Show()
+ ENDIF
+RETURN SELF
+
+METHOD Drop(oDragEvent)
+ IF File(oDragEvent:FileName(1))
+ SELF:DoOpenFile(oDragEvent:FileName(1))
+ ENDIF
+RETURN SELF
+
+METHOD FileClose()
+ SELF:Use()
+RETURN SELF
+
+METHOD FileExit()
+ SELF:EndWindow()
+RETURN SELF
+
+METHOD FileOpen()
+ LOCAL oOD AS OpenDialog
+ LOCAL oTB AS TextBox
+ LOCAL retval AS SHORT
+
+ IF(oAttachedServer == NULL_OBJECT)
+ (oOD := OpenDialog{SELF, "*.dbf"}):Show()
+
+ IF !Empty(oOD:FileName)
+ SELF:DoOpenFile(oOD:FileName, oOD:ReadOnly)
+ ENDIF
+ ELSE
+ oTB := TextBox{SELF, "Warning", "A DBF file is already open!" + _CHR(10)+ "Do you want close it?"}
+ oTB:Type := BUTTONOKAYCANCEL
+ retval := oTB:Show()
+ IF retval == BOXREPLYCANCEL
+ RETURN SELF
+ ENDIF
+ IF retVal==BOXREPLYOKAY
+ (oOD := OpenDialog{SELF, "*.dbf"}):Show()
+ IF !Empty(oOD:FileName)
+ SELF:DoOpenFile(oOD:FileName, oOD:ReadOnly)
+ ENDIF
+ ENDIF
+ ENDIF
+RETURN SELF
+
+METHOD FilePrint()
+ SELF:Print(oPrinter)
+RETURN SELF
+
+METHOD FilePrinterSetup()
+
+ oPrinter:Setup()
+
+RETURN SELF
+
+CONSTRUCTOR(oOwnerApp)
+
+ SetDeleted(TRUE)
+
+ SUPER(oOwnerApp)
+
+ SELF:EnableDragDropClient()
+
+ SELF:Icon := Icon{ResourceID{IDI_STANDARDICON, _GetInst()}}
+ SELF:Menu := EmptySDIMenu{SELF}
+ oStdMenu := StandardSDIMenu{SELF}
+ SELF:Caption := "VO SDI Application"
+ SELF:EnableStatusBar(TRUE)
+ SELF:StatusBar:DisplayTime(TRUE)
+ SELF:QuitOnClose := TRUE
+
+ SELF:Size := Dimension{850,650}
+
+ oPrinter := PrintingDevice{}
+
+ RETURN SELF
+
+
+method ViewForm()
+ self:ToolBar:UnPressItem(IDM_StandardSDIMenu_View_Table_ID)
+ self:ToolBar:PressItem(IDM_StandardSDIMenu_View_Form_ID)
+
+ return super:ViewForm()
+
+method ViewTable()
+ self:ToolBar:UnPressItem(IDM_StandardSDIMenu_View_Form_ID)
+ self:ToolBar:PressItem(IDM_StandardSDIMenu_View_Table_ID)
+
+ return super:ViewTable()
+
+
+
+END CLASS
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Start.prg b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Start.prg
new file mode 100644
index 0000000000..340a15d66c
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/Start.prg
@@ -0,0 +1,24 @@
+[STAThread];
+FUNCTION Start() AS INT
+ LOCAL oXApp AS XApp
+ TRY
+ oXApp := XApp{}
+ oXApp:Start()
+ CATCH oException AS Exception
+ ErrorDialog(oException)
+ END TRY
+RETURN 0
+
+CLASS XApp INHERIT App
+METHOD Start()
+ LOCAL oMainWindow AS StandardSDIWindow
+
+ oMainWindow := StandardSDIWindow{SELF}
+ oMainWindow:Show(SHOWCENTERED)
+
+ SELF:Exec()
+
+RETURN SELF
+
+
+END CLASS
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/VO/VOTestBed.xsproj b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/VOTestBed.xsproj
new file mode 100644
index 0000000000..5e87d2fc5b
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/VO/VOTestBed.xsproj
@@ -0,0 +1,52 @@
+
+
+
+ WinExe
+ net48
+ x86
+ VOTestBed
+ VO
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ false
+ false
+ true
+ false
+ true
+ true
+ false
+ $(NoWarn);9032;9043;618;162;165;649;9025
+
+
+
+ $(registry:HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\XSharpBV\XSharp@XSharpPath)
+ $(MSBuildProgramFiles32)\XSharp
+ $([MSBuild]::EnsureTrailingSlash('$(XSharpInstallDir)'))Assemblies\
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/Form1.designer.prg b/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/Form1.designer.prg
new file mode 100644
index 0000000000..456dc884f3
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/Form1.designer.prg
@@ -0,0 +1,36 @@
+BEGIN NAMESPACE WinFormsTestBed
+
+ PARTIAL CLASS Form1
+
+ PRIVATE components := NULL AS System.ComponentModel.IContainer
+ PRIVATE button1 AS System.Windows.Forms.Button
+
+ PROTECTED OVERRIDE METHOD Dispose(disposing AS LOGIC) AS VOID STRICT
+ IF (disposing .AND. (components != NULL))
+ components:Dispose()
+ ENDIF
+ SUPER:Dispose(disposing)
+ RETURN
+ END METHOD
+
+ #region Windows Form Designer generated code
+ PRIVATE METHOD InitializeComponent() AS VOID STRICT
+ SELF:button1 := System.Windows.Forms.Button{}
+ SELF:SuspendLayout()
+ SELF:button1:Location := System.Drawing.Point{12, 12}
+ SELF:button1:Name := "button1"
+ SELF:button1:Size := System.Drawing.Size{120, 30}
+ SELF:button1:TabIndex := 0
+ SELF:button1:Text := "Hello"
+ SELF:AutoScaleMode := System.Windows.Forms.AutoScaleMode.Font
+ SELF:ClientSize := System.Drawing.Size{300, 160}
+ SELF:Controls:Add(SELF:button1)
+ SELF:Name := "Form1"
+ SELF:Text := "X# CPS WinForms"
+ SELF:ResumeLayout(FALSE)
+ RETURN
+ END METHOD
+ #endregion
+ END CLASS
+
+END NAMESPACE
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/Form1.prg b/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/Form1.prg
new file mode 100644
index 0000000000..b3f69dd891
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/Form1.prg
@@ -0,0 +1,13 @@
+USING System
+USING System.Windows.Forms
+
+BEGIN NAMESPACE WinFormsTestBed
+
+ PUBLIC PARTIAL CLASS Form1 INHERIT System.Windows.Forms.Form
+
+ PUBLIC CONSTRUCTOR() STRICT
+ SELF:InitializeComponent()
+ RETURN
+ END CONSTRUCTOR
+ END CLASS
+END NAMESPACE
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/Program.prg b/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/Program.prg
new file mode 100644
index 0000000000..d2c8701b44
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/Program.prg
@@ -0,0 +1,10 @@
+USING System
+USING System.Windows.Forms
+USING WinFormsTestBed
+
+[STAThread] ;
+FUNCTION Start() AS VOID
+ Application.EnableVisualStyles()
+ Application.SetCompatibleTextRenderingDefault(FALSE)
+ Application.Run(Form1{})
+ RETURN
diff --git a/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/WinFormsTestBed.xsproj b/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/WinFormsTestBed.xsproj
new file mode 100644
index 0000000000..9c6a5d06b9
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS.TestBed/WinForms/WinFormsTestBed.xsproj
@@ -0,0 +1,16 @@
+
+
+
+ WinExe
+ net8.0-windows
+ true
+ Core
+ WinFormsTestBed
+ True
+ True
+
+
\ No newline at end of file
diff --git a/src/VisualStudio/ProjectSystemCPS/Evaluation/XSharpCpsGlobalPropertiesProvider.cs b/src/VisualStudio/ProjectSystemCPS/Evaluation/XSharpCpsGlobalPropertiesProvider.cs
new file mode 100644
index 0000000000..5bf4419f62
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/Evaluation/XSharpCpsGlobalPropertiesProvider.cs
@@ -0,0 +1,48 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Immutable;
+using System.ComponentModel.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.ProjectSystem;
+using Microsoft.VisualStudio.ProjectSystem.Build;
+
+namespace XSharp.ProjectSystem.Evaluation
+{
+ ///
+ /// Adds the global property XSharpCpsProjectSystem=true to the MSBuild evaluation of projects that are
+ /// loaded by the X# CPS project type.
+ ///
+ ///
+ /// The X# targets (XSharp.CurrentVersion.targets) only import XSharp.DesignTime.targets, and with it the
+ /// managed design-time targets of Visual Studio, when this property is set. Projects loaded by the MPFproj
+ /// project system and command line builds are therefore not affected.
+ /// The provider applies to the initial capability of the project type, so it is active for the first evaluation.
+ ///
+ [Export(typeof(IProjectGlobalPropertiesProvider))]
+ [AppliesTo(XSharpCapabilities.XSharpCps)]
+ internal sealed class XSharpCpsGlobalPropertiesProvider : StaticGlobalPropertiesProviderBase
+ {
+ internal const string PropertyName = "XSharpCpsProjectSystem";
+
+ private static readonly Task> properties =
+ Task.FromResult>(
+ ImmutableDictionary.Create(StringComparer.OrdinalIgnoreCase).Add(PropertyName, "true"));
+
+ [ImportingConstructor]
+ public XSharpCpsGlobalPropertiesProvider(IProjectService projectService)
+ : base(projectService.Services)
+ {
+ }
+
+ public override Task> GetGlobalPropertiesAsync(CancellationToken cancellationToken)
+ {
+ return properties;
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/Imaging/XSharpImages.cs b/src/VisualStudio/ProjectSystemCPS/Imaging/XSharpImages.cs
new file mode 100644
index 0000000000..231cca5642
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/Imaging/XSharpImages.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using Microsoft.VisualStudio.ProjectSystem;
+
+namespace XSharp.ProjectSystem.Imaging
+{
+ ///
+ /// Image monikers of XSharp.ProjectSystemCPS.imagemanifest.
+ ///
+ internal static class XSharpImages
+ {
+ public static readonly Guid ImageCatalogGuid = new Guid("D9769BF1-DF1F-48B6-9642-949FB27BF46D");
+
+ public static readonly ProjectImageMoniker Project = new ProjectImageMoniker(ImageCatalogGuid, 1);
+ public static readonly ProjectImageMoniker Document = new ProjectImageMoniker(ImageCatalogGuid, 2);
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/Imaging/XSharpProjectTreePropertiesProvider.cs b/src/VisualStudio/ProjectSystemCPS/Imaging/XSharpProjectTreePropertiesProvider.cs
new file mode 100644
index 0000000000..12e157136f
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/Imaging/XSharpProjectTreePropertiesProvider.cs
@@ -0,0 +1,243 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.VisualStudio.Imaging;
+using Microsoft.VisualStudio.ProjectSystem;
+using XSharp.CodeDom;
+
+namespace XSharp.ProjectSystem.Imaging
+{
+ ///
+ /// Sets the X# icons in Solution Explorer, like XSharpFileNode does in MPFproj: the project node, forms and
+ /// user controls, the VO binaries and the other X# source files.
+ ///
+ ///
+ /// The managed project system provides the project icon through IProjectImageProvider, which is internal
+ /// to it and only implemented for C#, VB and F#. IProjectTreePropertiesProvider is the public CPS way.
+ /// A .prg is a form/user control when its SubType says so, or -- SDK-style projects rarely have a SubType --
+ /// when it has a matching .Designer.prg and its class inherits from Form/UserControl (same inference as the
+ /// MPFproj SDK project node).
+ /// Not a MEF export: supplies it, so that the icons of
+ /// all nodes are calculated again when a .Designer.prg is added or removed.
+ ///
+ internal sealed class XSharpProjectTreePropertiesProvider : IProjectTreePropertiesProvider
+ {
+ private static readonly ProjectImageMoniker WindowsForm = Known(KnownImageIds.WindowsForm);
+ private static readonly ProjectImageMoniker UserControl = Known(KnownImageIds.UserControl);
+
+ private static readonly Dictionary ExtensionIcons =
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { ".prg", XSharpImages.Document },
+ { ".prgx", XSharpImages.Document },
+ { ".xs", XSharpImages.Document },
+ { ".xsfrm", Known(KnownImageIds.FormInstance) },
+ { ".vnfrm", Known(KnownImageIds.FormInstance) },
+ { ".xsdbs", Known(KnownImageIds.Database) },
+ { ".vndbs", Known(KnownImageIds.Database) },
+ { ".xssql", Known(KnownImageIds.Database) },
+ { ".vnsqs", Known(KnownImageIds.Database) },
+ { ".xsmnu", Known(KnownImageIds.MainMenuControl) },
+ { ".vnmnu", Known(KnownImageIds.MainMenuControl) },
+ { ".xsfs", Known(KnownImageIds.ValidationRule) },
+ { ".vnfs", Known(KnownImageIds.ValidationRule) },
+ { ".xsrep", Known(KnownImageIds.Report) },
+ { ".vnrep", Known(KnownImageIds.Report) },
+ };
+
+ // This provider runs synchronously on the CPS tree thread for the nodes of each tree update (all nodes when
+ // the provider instance changes), so it keeps file system access low: the designer files are looked up in a
+ // cached list per folder, and a form's file is only read until its CLASS declaration. Both caches are
+ // cleared when they grow beyond MaxCacheEntries.
+ private const int MaxCacheEntries = 1000;
+ // A folder listing is reused for this long, then again as long as the folder's timestamp is unchanged
+ // (on NTFS it changes when a file in it is created, deleted or renamed). Short: it only has to cover the burst
+ // of one tree update (all nodes within milliseconds); a longer interval could miss a .Designer.prg that was
+ // just created (Add New Item) and leave the form icon generic until the next tree update.
+ private static readonly TimeSpan FolderRecheckInterval = TimeSpan.FromMilliseconds(250);
+ // Longest part of a form's file that is searched for the CLASS declaration
+ private const int MaxClassSearchLength = 64 * 1024;
+
+ // Result of the base class inference per file, invalidated by the file's timestamp
+ private static readonly ConcurrentDictionary inferredSubTypes =
+ new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+
+ // *.Designer.prg file names per folder
+ private static readonly ConcurrentDictionary Names)> designerFilesByFolder =
+ new ConcurrentDictionary)>(StringComparer.OrdinalIgnoreCase);
+
+ // The X# CodeDOM generator (form designers, "Add .designer file") writes the declaration over two lines, with a
+ // line continuation: "CLASS Form1 ;" and "INHERIT System.Windows.Forms.Form"
+ private static readonly Regex BaseClassRegex =
+ new Regex(@"\bCLASS\s+[^\s;]+[\s;]+INHERIT\s+([A-Za-z_][\w.]*)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ private readonly UnconfiguredProject project;
+
+ public XSharpProjectTreePropertiesProvider(UnconfiguredProject project)
+ {
+ this.project = project;
+ }
+
+ private static ProjectImageMoniker Known(int id) => new ProjectImageMoniker(KnownImageIds.ImageCatalogGuid, id);
+
+ public void CalculatePropertyValues(IProjectTreeCustomizablePropertyContext propertyContext, IProjectTreeCustomizablePropertyValues propertyValues)
+ {
+ if (propertyValues.Flags.Contains(ProjectTreeFlags.ProjectRoot))
+ {
+ propertyValues.Icon = XSharpImages.Project;
+ return;
+ }
+ if (propertyValues.Flags.Contains(ProjectTreeFlags.Folder) || string.IsNullOrEmpty(propertyContext.ItemName))
+ return;
+ var extension = Path.GetExtension(propertyContext.ItemName);
+ if (!ExtensionIcons.TryGetValue(extension, out var icon))
+ return;
+ if (string.Equals(extension, ".prg", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (GetDesignerSubType(propertyContext))
+ {
+ case "Form":
+ icon = WindowsForm;
+ break;
+ case "UserControl":
+ icon = UserControl;
+ break;
+ }
+ }
+ propertyValues.Icon = icon;
+ propertyValues.ExpandedIcon = icon;
+ }
+
+ private string GetDesignerSubType(IProjectTreeCustomizablePropertyContext propertyContext)
+ {
+ if (propertyContext.Metadata != null && propertyContext.Metadata.TryGetValue("SubType", out var subType) &&
+ !string.IsNullOrEmpty(subType))
+ {
+ return subType;
+ }
+ // ItemName is only the file name; the full path is in the FullPath metadata (as used by the managed
+ // project system). Fall back to the project folder for items without metadata.
+ string path = null;
+ if (propertyContext.Metadata == null || !propertyContext.Metadata.TryGetValue("FullPath", out path) || string.IsNullOrEmpty(path))
+ path = Path.Combine(Path.GetDirectoryName(project.FullPath), propertyContext.ItemName);
+ if (!HasDesignerFile(path))
+ return null;
+ return InferSubTypeFromBaseClass(path);
+ }
+
+ ///
+ /// Like ShadowDesignerBridge.HasDesignerFile, but with the cached folder listing (see above). The bridge
+ /// itself stays uncached: the commands and NewFormDesignerRedirect need the current state.
+ ///
+ private static bool HasDesignerFile(string prgPath)
+ {
+ try
+ {
+ if (!string.Equals(Path.GetExtension(prgPath), ".prg", StringComparison.OrdinalIgnoreCase) ||
+ prgPath.EndsWith(".designer.prg", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ var folder = Path.GetDirectoryName(prgPath);
+ var designerName = Path.GetFileName(XSharpCodeDomHelper.BuildDesignerFileName(prgPath));
+ if (string.IsNullOrEmpty(folder) || string.IsNullOrEmpty(designerName))
+ return false;
+ return GetDesignerFileNames(folder).Contains(designerName);
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ private static HashSet GetDesignerFileNames(string folder)
+ {
+ var now = DateTime.UtcNow;
+ if (designerFilesByFolder.TryGetValue(folder, out var cached) && now - cached.Checked < FolderRecheckInterval)
+ return cached.Names;
+ var stamp = Directory.GetLastWriteTimeUtc(folder);
+ HashSet names;
+ if (cached.Names != null && cached.Stamp == stamp)
+ {
+ names = cached.Names;
+ }
+ else
+ {
+ names = new HashSet(
+ Directory.EnumerateFiles(folder, "*.designer.prg").Select(Path.GetFileName),
+ StringComparer.OrdinalIgnoreCase);
+ }
+ if (designerFilesByFolder.Count > MaxCacheEntries)
+ designerFilesByFolder.Clear();
+ designerFilesByFolder[folder] = (now, stamp, names);
+ return names;
+ }
+
+ ///
+ /// "CLASS Foo INHERIT System.Windows.Forms.Form" -> Form, "... INHERIT UserControl" -> UserControl.
+ /// Same rule as XSharpFileNode.InferSubTypeFromBaseClass (MPFproj).
+ ///
+ private static string InferSubTypeFromBaseClass(string path)
+ {
+ try
+ {
+ var stamp = File.GetLastWriteTimeUtc(path);
+ if (inferredSubTypes.TryGetValue(path, out var cached) && cached.Stamp == stamp)
+ return cached.SubType;
+ string subType = null;
+ var match = FindClassDeclaration(path);
+ if (match != null)
+ {
+ var baseType = match.Groups[1].Value;
+ if (baseType.Equals("Form", StringComparison.OrdinalIgnoreCase) || baseType.EndsWith(".Form", StringComparison.OrdinalIgnoreCase))
+ subType = "Form";
+ else if (baseType.Equals("UserControl", StringComparison.OrdinalIgnoreCase) || baseType.EndsWith(".UserControl", StringComparison.OrdinalIgnoreCase))
+ subType = "UserControl";
+ }
+ if (inferredSubTypes.Count > MaxCacheEntries)
+ inferredSubTypes.Clear();
+ inferredSubTypes[path] = (stamp, subType);
+ return subType;
+ }
+ catch (Exception)
+ {
+ // Best effort, a failure must never break the tree
+ return null;
+ }
+ }
+
+ ///
+ /// The first "CLASS ... INHERIT ..." of the file. Reads the file in blocks and stops at the first match (a form's
+ /// class is near the top) or after characters, instead of reading it all.
+ ///
+ private static Match FindClassDeclaration(string path)
+ {
+ var text = new StringBuilder();
+ var buffer = new char[4096];
+ using (var reader = new StreamReader(path, detectEncodingFromByteOrderMarks: true))
+ {
+ int read;
+ while (text.Length < MaxClassSearchLength && (read = reader.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ text.Append(buffer, 0, read);
+ // The whole text so far: a declaration can span two blocks
+ var match = BaseClassRegex.Match(text.ToString());
+ if (match.Success)
+ return match;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/Imaging/XSharpProjectTreePropertiesProviderSource.cs b/src/VisualStudio/ProjectSystemCPS/Imaging/XSharpProjectTreePropertiesProviderSource.cs
new file mode 100644
index 0000000000..fcf230322f
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/Imaging/XSharpProjectTreePropertiesProviderSource.cs
@@ -0,0 +1,103 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Immutable;
+using System.ComponentModel.Composition;
+using System.Threading.Tasks.Dataflow;
+using Microsoft.VisualStudio.ProjectSystem;
+using XSharp.Settings;
+
+namespace XSharp.ProjectSystem.Imaging
+{
+ ///
+ /// Supplies to the project tree, and a new instance of it when
+ /// the set of .Designer.prg files of the project changes.
+ ///
+ ///
+ /// CPS calls the property providers only for the nodes that changed in a tree update. "Add .designer file" adds a
+ /// .Designer.prg below an existing .prg: that adds a child node but leaves the node of the .prg unchanged, so its
+ /// form icon only appeared after the project was loaded again. When the list of property providers changes, CPS
+ /// calculates the properties of all nodes again (PhysicalProjectTreeProvider.OnProjectTreePropertiesProvidersChanged),
+ /// comparing the providers by instance. So this data source publishes a new provider only when a .Designer.prg was
+ /// added or removed.
+ /// The values carry their own version, not the versions of the source items subscription: the tree input
+ /// synchronizes its sources by their data source versions (SyncLinkTo), and with the versions of the source items
+ /// it waited for ever -- the tree was never published and VS hung while restoring the open documents (verified in
+ /// VS). The first provider is published right away, the tree waits for one from every source.
+ ///
+ [Export(typeof(IProjectTreePropertiesProviderDataSource))]
+ [AppliesTo(XSharpCapabilities.XSharpCps)]
+ [Order(1000)]
+ internal sealed class XSharpProjectTreePropertiesProviderSource : ChainedProjectValueDataSourceBase, IProjectTreePropertiesProviderDataSource
+ {
+ private static readonly NamedIdentity VersionKey = new NamedIdentity("XSharpProjectTreePropertiesProvider");
+
+ private readonly UnconfiguredProject project;
+ private readonly IActiveConfiguredProjectSubscriptionService subscriptions;
+ // Only used by the action block, which handles one update at a time (and before it is linked)
+ private long version;
+ private ImmutableHashSet designerFiles;
+
+ [ImportingConstructor]
+ public XSharpProjectTreePropertiesProviderSource(UnconfiguredProject project, IActiveConfiguredProjectSubscriptionService subscriptions)
+ : base(project, synchronousDisposal: false, registerDataSource: false)
+ {
+ this.project = project;
+ this.subscriptions = subscriptions;
+ }
+
+ protected override IDisposable LinkExternalInput(ITargetBlock> targetBlock)
+ {
+ targetBlock.Post(CreateValue());
+ var action = DataflowBlockSlim.CreateActionBlock>(update =>
+ {
+ if (DesignerFilesChanged(update.Value))
+ targetBlock.Post(CreateValue());
+ });
+ // Like the adapter: the source item rules are dynamic, their names come from SourceItemRuleNamesSource
+ return subscriptions.SourceItemsRuleSource.SourceBlock.LinkTo(action, subscriptions.SourceItemRuleNamesSource.SourceBlock,
+ new DataflowLinkOptions { PropagateCompletion = true });
+ }
+
+ private IProjectVersionedValue CreateValue()
+ {
+ version++;
+ return new ProjectVersionedValue(new XSharpProjectTreePropertiesProvider(project),
+ ImmutableDictionary.Empty.Add(VersionKey, version));
+ }
+
+ ///
+ /// True when a .Designer.prg was added or removed since the last update. The first update only records them:
+ /// the first provider was applied to all nodes anyway.
+ ///
+ private bool DesignerFilesChanged(IProjectSubscriptionUpdate update)
+ {
+ try
+ {
+ var builder = ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase);
+ foreach (var rule in update.CurrentState.Values)
+ {
+ foreach (var item in rule.Items.Keys)
+ {
+ if (item.EndsWith(".designer.prg", StringComparison.OrdinalIgnoreCase))
+ builder.Add(item);
+ }
+ }
+ var current = builder.ToImmutable();
+ var previous = designerFiles;
+ designerFiles = current;
+ return previous != null && !current.SetEquals(previous);
+ }
+ catch (Exception e)
+ {
+ // Keep the provider: a failure must never break the tree
+ XSettings.Exception(e);
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/LanguageService/BuildErrorLoggerProvider.cs b/src/VisualStudio/ProjectSystemCPS/LanguageService/BuildErrorLoggerProvider.cs
new file mode 100644
index 0000000000..1edb71e223
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/LanguageService/BuildErrorLoggerProvider.cs
@@ -0,0 +1,166 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Build.Framework;
+using Microsoft.VisualStudio.ProjectSystem;
+using Microsoft.VisualStudio.ProjectSystem.Build;
+using XSharp.Settings;
+using XSharpModel;
+using ILogger = Microsoft.Build.Framework.ILogger;
+
+namespace XSharp.ProjectSystem.LanguageService
+{
+ ///
+ /// Collects the errors and warnings of real builds of an X# CPS project, for the error squiggles in the editor.
+ ///
+ ///
+ /// In MPFproj the build logger (XSharpIDEBuildLogger) fills the ErrorListManager, and
+ /// XSharpProjectNode.GetIntellisenseErrors returns these build errors to XSharpErrorColorizer. Under CPS the build
+ /// errors only reach the Error List of VS, so this logger keeps a copy for
+ /// . Like XSharpIDEBuildLogger the errors of a
+ /// configuration are replaced when it is built again. Design-time builds are not logged (they do not run the
+ /// compiler and use other targets).
+ ///
+ [Export(typeof(IBuildLoggerProviderAsync))]
+ [AppliesTo(XSharpCapabilities.XSharpCps)]
+ internal sealed class BuildErrorLoggerProvider : IBuildLoggerProviderAsync
+ {
+ private static readonly Task> noLoggers =
+ Task.FromResult>(ImmutableHashSet.Empty);
+
+ private static readonly HashSet BuildTargets = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Build", "Rebuild", "Clean",
+ };
+
+ private readonly ConfiguredProject configuredProject;
+
+ [ImportingConstructor]
+ public BuildErrorLoggerProvider(ConfiguredProject configuredProject)
+ {
+ this.configuredProject = configuredProject;
+ }
+
+ public Task> GetLoggersAsync(IReadOnlyList targets, IImmutableDictionary properties, CancellationToken cancellationToken)
+ {
+ try
+ {
+ if (targets == null || !targets.SelectMany(t => t.Split(';')).Any(t => BuildTargets.Contains(t.Trim())))
+ return noLoggers;
+ if (properties != null && properties.TryGetValue("DesignTimeBuild", out var designTime) &&
+ string.Equals(designTime, "true", StringComparison.OrdinalIgnoreCase))
+ return noLoggers;
+ var projectFile = configuredProject.UnconfiguredProject.FullPath;
+ if (!(XSolution.FindProjectByFileName(projectFile)?.ProjectNode is XSharpProjectAdapter adapter))
+ return noLoggers;
+ var logger = new BuildErrorLogger(adapter, configuredProject.ProjectConfiguration?.Name, projectFile);
+ return Task.FromResult>(ImmutableHashSet.Create(logger));
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ return noLoggers;
+ }
+ }
+
+ private sealed class BuildErrorLogger : ILogger
+ {
+ private readonly XSharpProjectAdapter adapter;
+ private readonly string configuration;
+ private readonly string projectFile;
+ private readonly string projectFolder;
+ private readonly List positions = new List();
+ private IEventSource eventSource;
+
+ public BuildErrorLogger(XSharpProjectAdapter adapter, string configuration, string projectFile)
+ {
+ this.adapter = adapter;
+ this.configuration = configuration;
+ this.projectFile = projectFile;
+ projectFolder = Path.GetDirectoryName(projectFile);
+ }
+
+ public LoggerVerbosity Verbosity { get; set; } = LoggerVerbosity.Quiet;
+ public string Parameters { get; set; }
+
+ public void Initialize(IEventSource eventSource)
+ {
+ this.eventSource = eventSource;
+ eventSource.ErrorRaised += OnErrorRaised;
+ eventSource.WarningRaised += OnWarningRaised;
+ eventSource.ProjectFinished += OnProjectFinished;
+ eventSource.BuildFinished += OnBuildFinished;
+ }
+
+ public void Shutdown()
+ {
+ if (eventSource != null)
+ {
+ eventSource.ErrorRaised -= OnErrorRaised;
+ eventSource.WarningRaised -= OnWarningRaised;
+ eventSource.ProjectFinished -= OnProjectFinished;
+ eventSource.BuildFinished -= OnBuildFinished;
+ eventSource = null;
+ }
+ Commit();
+ }
+
+ ///
+ /// Publishes the errors collected so far. Called when this project finishes, when the build finishes and
+ /// at shutdown; each call replaces the previous result, so repeated calls are harmless.
+ /// Also after a failed or cancelled build: the errors of the previous build are outdated.
+ ///
+ private void Commit()
+ {
+ try
+ {
+ List result;
+ lock (positions)
+ {
+ result = positions.ToList();
+ }
+ adapter.SetBuildErrors(configuration, result);
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ }
+ }
+
+ private void OnProjectFinished(object sender, ProjectFinishedEventArgs e)
+ {
+ if (string.Equals(e.ProjectFile, projectFile, StringComparison.OrdinalIgnoreCase))
+ Commit();
+ }
+
+ private void OnBuildFinished(object sender, BuildFinishedEventArgs e) => Commit();
+
+ private void OnErrorRaised(object sender, BuildErrorEventArgs e) => Add(e.File, e.ProjectFile, e.LineNumber, e.ColumnNumber);
+
+ private void OnWarningRaised(object sender, BuildWarningEventArgs e) => Add(e.File, e.ProjectFile, e.LineNumber, e.ColumnNumber);
+
+ private void Add(string file, string projectFile, int line, int column)
+ {
+ if (string.IsNullOrEmpty(file) || line <= 0)
+ return;
+ var folder = string.IsNullOrEmpty(projectFile) ? projectFolder : Path.GetDirectoryName(projectFile);
+ var position = new BuildErrorPosition(XSharpCommandLine.MakeFullPath(file, folder), line, Math.Max(column, 1));
+ lock (positions)
+ {
+ positions.Add(position);
+ }
+ }
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/LanguageService/CommentTaskList.cs b/src/VisualStudio/ProjectSystemCPS/LanguageService/CommentTaskList.cs
new file mode 100644
index 0000000000..b93b41ef4b
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/LanguageService/CommentTaskList.cs
@@ -0,0 +1,115 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Microsoft.VisualStudio;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Threading;
+using XSharpModel;
+
+namespace XSharp.ProjectSystem.LanguageService
+{
+ ///
+ /// Shows the comment tasks (TODO, HACK etc.) of a CPS project in the Task List.
+ ///
+ ///
+ /// Replaces the TaskListManager of MPFproj (ProjectPackage), which XSharpProjectNode fills after each
+ /// project or file walk. Updates are coalesced and applied on the UI thread.
+ ///
+ internal sealed class CommentTaskList : IDisposable
+ {
+ private readonly JoinableTaskFactory joinableTaskFactory;
+ private readonly string projectName;
+ private TaskProvider provider;
+ private IList pending;
+ private int refreshScheduled;
+ // Written by Dispose on any thread, read by RefreshAsync on the UI thread
+ private volatile bool disposed;
+
+ public CommentTaskList(JoinableTaskFactory joinableTaskFactory, string projectName)
+ {
+ this.joinableTaskFactory = joinableTaskFactory;
+ this.projectName = projectName;
+ }
+
+ public void Update(IList tasks)
+ {
+ Volatile.Write(ref pending, tasks);
+ if (Interlocked.Exchange(ref refreshScheduled, 1) == 0)
+ {
+ joinableTaskFactory.RunAsync(RefreshAsync).FileAndForget("XSharp/ProjectSystemCPS/TaskList");
+ }
+ }
+
+ private async System.Threading.Tasks.Task RefreshAsync()
+ {
+ await joinableTaskFactory.SwitchToMainThreadAsync();
+ Interlocked.Exchange(ref refreshScheduled, 0);
+ if (disposed)
+ return;
+ var tasks = Volatile.Read(ref pending) ?? Array.Empty();
+ if (provider == null)
+ {
+ provider = new TaskProvider(ServiceProvider.GlobalProvider)
+ {
+ ProviderName = "X# Comments (" + projectName + ")",
+ };
+ }
+ provider.SuspendRefresh();
+ try
+ {
+ provider.Tasks.Clear();
+ foreach (var commentTask in tasks)
+ {
+ if (commentTask?.File == null)
+ continue;
+ var task = new TaskListItem
+ {
+ Document = commentTask.File.FullPath,
+ // XCommentTask positions are 1-based, the task list is 0-based
+ Line = Math.Max(0, commentTask.Line - 1),
+ Column = Math.Max(0, commentTask.Column - 1),
+ Text = commentTask.Comment,
+ Priority = (TaskPriority)commentTask.Priority,
+ Category = TaskCategory.Comments,
+ };
+ task.Navigate += (sender, e) =>
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ provider?.Navigate((TaskListItem)sender, VSConstants.LOGVIEWID.TextView_guid);
+ };
+ provider.Tasks.Add(task);
+ }
+ }
+ finally
+ {
+ provider.ResumeRefresh();
+ }
+ }
+
+ ///
+ /// Removes the entries. The provider is only touched on the UI thread, after disposed is set: a
+ /// refresh that runs before this sees the flag or has already created the provider, which is then disposed here.
+ ///
+ public void Dispose()
+ {
+ disposed = true;
+ joinableTaskFactory.RunAsync(async () =>
+ {
+ await joinableTaskFactory.SwitchToMainThreadAsync();
+ var old = provider;
+ provider = null;
+ if (old != null)
+ {
+ old.Tasks.Clear();
+ old.Dispose();
+ }
+ }).FileAndForget("XSharp/ProjectSystemCPS/TaskList");
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/LanguageService/CommentTokens.cs b/src/VisualStudio/ProjectSystemCPS/LanguageService/CommentTokens.cs
new file mode 100644
index 0000000000..767eb14fc2
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/LanguageService/CommentTokens.cs
@@ -0,0 +1,60 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System.Collections.Generic;
+using Microsoft.VisualStudio;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using XSharp.Settings;
+using XSharpModel;
+
+namespace XSharp.ProjectSystem.LanguageService
+{
+ ///
+ /// The comment tokens (TODO, HACK etc.) of the code model's parser.
+ ///
+ ///
+ /// Saved files are walked again by (it replaced the editor save watcher that lived
+ /// in this file), which refreshes the comment tasks in the Task List.
+ ///
+ internal static class CommentTokens
+ {
+ ///
+ /// Passes the comment tokens (Tools > Options > Environment > Task List) to the code model when nobody did yet.
+ ///
+ ///
+ /// The X# project package sets them when an MPFproj project opens and after the options dialog closes
+ /// (XSharpProjectPackage.SetCommentTokens). In a solution with only CPS projects the parser would otherwise
+ /// find no comment tasks at all.
+ ///
+ public static void EnsureSet()
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ if (XSolution.CommentTokens.Count > 0 ||
+ !(ServiceProvider.GlobalProvider.GetService(typeof(SVsTaskList)) is IVsCommentTaskInfo taskInfo) ||
+ ErrorHandler.Failed(taskInfo.EnumTokens(out IVsEnumCommentTaskTokens enumTokens)) || enumTokens == null)
+ {
+ return;
+ }
+ var tokens = new List();
+ var buffer = new IVsCommentTaskToken[1];
+ while (enumTokens.Next(1, buffer, out uint fetched) == VSConstants.S_OK && fetched == 1)
+ {
+ var priority = new VSTASKPRIORITY[1];
+ if (ErrorHandler.Succeeded(buffer[0].Text(out string text)) && !string.IsNullOrEmpty(text) &&
+ ErrorHandler.Succeeded(buffer[0].Priority(priority)))
+ {
+ tokens.Add(new XCommentToken(text, (int)priority[0]));
+ }
+ }
+ if (tokens.Count > 0)
+ {
+ XSolution.SetCommentTokens(tokens);
+ XSettings.Information($"CommentTokens: {tokens.Count} comment tokens set");
+ }
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/LanguageService/IntellisenseErrorList.cs b/src/VisualStudio/ProjectSystemCPS/LanguageService/IntellisenseErrorList.cs
new file mode 100644
index 0000000000..4d25ea6297
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/LanguageService/IntellisenseErrorList.cs
@@ -0,0 +1,132 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Microsoft.VisualStudio;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Threading;
+using XSharpModel;
+using DiagnosticSeverity = LanguageService.CodeAnalysis.DiagnosticSeverity;
+
+namespace XSharp.ProjectSystem.LanguageService
+{
+ ///
+ /// Shows the intellisense errors of a CPS project in the Error List.
+ ///
+ ///
+ /// Replaces the ErrorListManager of MPFproj (ProjectPackage), which needs the MPFproj hierarchy.
+ /// Updates arrive on background threads (parser); they are coalesced and applied on the UI thread.
+ ///
+ internal sealed class IntellisenseErrorList : IDisposable
+ {
+ private readonly JoinableTaskFactory joinableTaskFactory;
+ private readonly string projectName;
+ private readonly Func> getErrors;
+ private ErrorListProvider provider;
+ private int refreshScheduled;
+ // Written by Dispose on any thread, read by RefreshAsync on the UI thread
+ private volatile bool disposed;
+
+ /// Returns the current errors of the project; called once per UI refresh.
+ public IntellisenseErrorList(JoinableTaskFactory joinableTaskFactory, string projectName, Func> getErrors)
+ {
+ this.joinableTaskFactory = joinableTaskFactory;
+ this.projectName = projectName;
+ this.getErrors = getErrors;
+ }
+
+ ///
+ /// The errors of the project changed: refresh the Error List (coalesced, on the UI thread).
+ ///
+ public void Update()
+ {
+ if (Interlocked.Exchange(ref refreshScheduled, 1) == 0)
+ {
+ joinableTaskFactory.RunAsync(RefreshAsync).FileAndForget("XSharp/ProjectSystemCPS/ErrorList");
+ }
+ }
+
+ private async System.Threading.Tasks.Task RefreshAsync()
+ {
+ await joinableTaskFactory.SwitchToMainThreadAsync();
+ Interlocked.Exchange(ref refreshScheduled, 0);
+ if (disposed)
+ return;
+ // Fetched after refreshScheduled was reset: a change after this point schedules the next refresh
+ var errors = getErrors() ?? Array.Empty();
+ if (provider == null)
+ {
+ provider = new ErrorListProvider(ServiceProvider.GlobalProvider)
+ {
+ ProviderName = "X# IntelliSense (" + projectName + ")",
+ };
+ }
+ provider.SuspendRefresh();
+ try
+ {
+ provider.Tasks.Clear();
+ foreach (var error in errors)
+ {
+ var task = new ErrorTask
+ {
+ Document = error.Path,
+ // XError positions are 1-based, the error list is 0-based
+ Line = Math.Max(0, error.Span.Line - 1),
+ Column = Math.Max(0, error.Span.Column - 1),
+ Text = string.IsNullOrEmpty(error.ErrCode) ? error.ToString() : error.ErrCode + ": " + error.ToString(),
+ ErrorCategory = ToCategory(error.Severity),
+ Category = TaskCategory.CodeSense,
+ };
+ task.Navigate += (sender, e) =>
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ provider?.Navigate((ErrorTask)sender, VSConstants.LOGVIEWID.TextView_guid);
+ };
+ provider.Tasks.Add(task);
+ }
+ }
+ finally
+ {
+ provider.ResumeRefresh();
+ }
+ }
+
+ private static TaskErrorCategory ToCategory(DiagnosticSeverity severity)
+ {
+ switch (severity)
+ {
+ case DiagnosticSeverity.Error:
+ return TaskErrorCategory.Error;
+ case DiagnosticSeverity.Warning:
+ return TaskErrorCategory.Warning;
+ default:
+ return TaskErrorCategory.Message;
+ }
+ }
+
+ ///
+ /// Removes the entries. The provider is only touched on the UI thread, after disposed is set: a
+ /// refresh that runs before this sees the flag or has already created the provider, which is then disposed here.
+ ///
+ public void Dispose()
+ {
+ disposed = true;
+ joinableTaskFactory.RunAsync(async () =>
+ {
+ await joinableTaskFactory.SwitchToMainThreadAsync();
+ var old = provider;
+ provider = null;
+ if (old != null)
+ {
+ old.Tasks.Clear();
+ old.Dispose();
+ }
+ }).FileAndForget("XSharp/ProjectSystemCPS/ErrorList");
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/LanguageService/IntellisenseErrorStore.cs b/src/VisualStudio/ProjectSystemCPS/LanguageService/IntellisenseErrorStore.cs
new file mode 100644
index 0000000000..6355bd326c
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/LanguageService/IntellisenseErrorStore.cs
@@ -0,0 +1,158 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using XSharpModel;
+
+namespace XSharp.ProjectSystem.LanguageService
+{
+ ///
+ /// Errors of the files of a CPS project for the editor (GetIntellisenseErrors, used for the error squiggles by
+ /// XSharpErrorColorizer) and the Error List.
+ ///
+ ///
+ /// The MPFproj project system keeps these in its ErrorListManager (ProjectPackage), which is tied to the
+ /// MPFproj hierarchy. Two sources, like there:
+ ///
+ /// - intellisense errors reported by the code model (AddIntellisenseError); every change is reported
+ /// through
(used for the Error List, see IntellisenseErrorList)
+ /// - errors and warnings of the last real build (
, see BuildErrorLoggerProvider).
+ /// Only for the squiggles: VS itself shows the build errors in the Error List.
+ ///
+ ///
+ internal sealed class IntellisenseErrorStore
+ {
+ private readonly Dictionary> errors = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ // Build errors per project configuration (one build per target framework), each keyed by the full file name
+ private readonly Dictionary> buildErrors = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Called after each change of the intellisense errors. The receiver fetches the errors with
+ /// when it needs them (IntellisenseErrorList coalesces the changes and fetches once per
+ /// UI refresh), so an Add does not copy all errors of the project.
+ ///
+ public Action Changed { get; set; }
+
+ ///
+ /// All intellisense errors of the project (a copy).
+ ///
+ public IReadOnlyList GetAll()
+ {
+ lock (errors)
+ {
+ return errors.Values.SelectMany(l => l).ToList();
+ }
+ }
+
+ public void Add(XError error)
+ {
+ if (error == null || string.IsNullOrEmpty(error.Path))
+ return;
+ lock (errors)
+ {
+ if (!errors.TryGetValue(error.Path, out var list))
+ {
+ list = new List();
+ errors.Add(error.Path, list);
+ }
+ list.Add(error);
+ }
+ OnChanged();
+ }
+
+ public void Clear(string fileName)
+ {
+ if (string.IsNullOrEmpty(fileName))
+ return;
+ bool removed;
+ lock (errors)
+ {
+ removed = errors.Remove(fileName);
+ }
+ if (removed)
+ OnChanged();
+ }
+
+ ///
+ /// Replaces the build errors of with the errors and warnings of its last build.
+ ///
+ public void SetBuildErrors(string configuration, IEnumerable positions)
+ {
+ var lookup = positions.ToLookup(p => p.FileName, p => (IXErrorPosition)p, StringComparer.OrdinalIgnoreCase);
+ lock (errors)
+ {
+ buildErrors[configuration ?? ""] = lookup;
+ }
+ }
+
+ private void OnChanged() => Changed?.Invoke();
+
+ public List Get(string fileName)
+ {
+ var result = new List();
+ if (string.IsNullOrEmpty(fileName))
+ return result;
+ // dedupe errors on the same position, like the ErrorListManager does
+ var positions = new HashSet<(int, int)>();
+ lock (errors)
+ {
+ if (errors.TryGetValue(fileName, out var list))
+ {
+ foreach (var error in list)
+ {
+ if (positions.Add((error.Span.Line, error.Span.Column)))
+ result.Add(new ErrorPosition(error.Span.Line, error.Span.Column, 1));
+ }
+ }
+ foreach (var lookup in buildErrors.Values)
+ {
+ foreach (var error in lookup[fileName])
+ {
+ if (positions.Add((error.Line, error.Column)))
+ result.Add(error);
+ }
+ }
+ }
+ return result;
+ }
+
+ private sealed class ErrorPosition : IXErrorPosition
+ {
+ public ErrorPosition(int line, int column, int length)
+ {
+ Line = line;
+ Column = column;
+ Length = length;
+ }
+
+ public int Column { get; set; }
+ public int Length { get; set; }
+ public int Line { get; set; }
+ }
+ }
+
+ ///
+ /// Position of a build error or warning (1-based, like the MSBuild events and the code model errors).
+ ///
+ internal sealed class BuildErrorPosition : IXErrorPosition
+ {
+ public BuildErrorPosition(string fileName, int line, int column)
+ {
+ FileName = fileName;
+ Line = line;
+ Column = column;
+ Length = 1;
+ }
+
+ public string FileName { get; }
+ public int Column { get; set; }
+ public int Length { get; set; }
+ public int Line { get; set; }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/LanguageService/SourceFileWatcher.cs b/src/VisualStudio/ProjectSystemCPS/LanguageService/SourceFileWatcher.cs
new file mode 100644
index 0000000000..c51a5ddb30
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/LanguageService/SourceFileWatcher.cs
@@ -0,0 +1,136 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using XSharp.Settings;
+
+namespace XSharp.ProjectSystem.LanguageService
+{
+ ///
+ /// Reports source files of an X# CPS project that changed on disk, so the adapter can walk them again.
+ ///
+ ///
+ /// MPFproj does this in XSharpProjectNode.OnFileChanged (file change notifications for its items, then
+ /// WalkFile(file, notify: true)). Under CPS nobody re-read a file that changed on disk: the VO designers write the
+ /// generated .prg directly to disk when it is not open in an editor, and the code model kept the old types until
+ /// the next project walk (verified in VS). The same applies to saves from the VS editor, external editors and
+ /// source control operations, which this watcher covers as well.
+ /// A FileSystemWatcher on the project folder (including subfolders); the adapter decides which paths are source
+ /// files of the project. Changes are collected and reported after , so a burst of writes
+ /// (a designer save writes several files, editors write via temporary files) gives one walk per file.
+ /// Files outside the project folder (links) are not watched.
+ ///
+ internal sealed class SourceFileWatcher : IDisposable
+ {
+ private static readonly TimeSpan Delay = TimeSpan.FromMilliseconds(500);
+
+ private readonly Func isSourceFile;
+ private readonly Action fileChanged;
+ private readonly Action changesLost;
+ private readonly FileSystemWatcher watcher;
+ private readonly Timer timer;
+ private readonly object gate = new object();
+ private readonly HashSet pending = new HashSet(StringComparer.OrdinalIgnoreCase);
+ private bool disposed;
+
+ /// The project folder.
+ /// True for the full path of a source file of the project (called on the watcher's thread).
+ /// Called with the full path of each changed source file, on a background thread.
+ /// Called when changes may have been missed (the watcher's buffer overflowed).
+ public SourceFileWatcher(string folder, Func isSourceFile, Action fileChanged, Action changesLost)
+ {
+ this.isSourceFile = isSourceFile;
+ this.fileChanged = fileChanged;
+ this.changesLost = changesLost;
+ timer = new Timer(OnTimer, null, Timeout.Infinite, Timeout.Infinite);
+ watcher = new FileSystemWatcher(folder)
+ {
+ IncludeSubdirectories = true,
+ NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size,
+ InternalBufferSize = 64 * 1024,
+ };
+ watcher.Changed += (sender, e) => Queue(e.FullPath);
+ watcher.Created += (sender, e) => Queue(e.FullPath);
+ watcher.Renamed += (sender, e) => Queue(e.FullPath);
+ watcher.Error += OnError;
+ watcher.EnableRaisingEvents = true;
+ }
+
+ private void Queue(string path)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(path) || !isSourceFile(path))
+ return;
+ lock (gate)
+ {
+ if (disposed)
+ return;
+ pending.Add(path);
+ timer.Change(Delay, Timeout.InfiniteTimeSpan);
+ }
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ }
+ }
+
+ private void OnTimer(object state)
+ {
+ string[] files;
+ lock (gate)
+ {
+ if (disposed)
+ return;
+ files = pending.ToArray();
+ pending.Clear();
+ }
+ foreach (var file in files)
+ {
+ try
+ {
+ fileChanged(file);
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ }
+ }
+ }
+
+ private void OnError(object sender, ErrorEventArgs e)
+ {
+ XSettings.Information("SourceFileWatcher: " + e.GetException()?.Message + ", walking the project again");
+ try
+ {
+ changesLost();
+ }
+ catch (Exception ex)
+ {
+ XSettings.Exception(ex);
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (gate)
+ {
+ if (disposed)
+ return;
+ disposed = true;
+ pending.Clear();
+ }
+ watcher.EnableRaisingEvents = false;
+ watcher.Dispose();
+ timer.Dispose();
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/LanguageService/XSharpCommandLine.cs b/src/VisualStudio/ProjectSystemCPS/LanguageService/XSharpCommandLine.cs
new file mode 100644
index 0000000000..be0680a51d
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/LanguageService/XSharpCommandLine.cs
@@ -0,0 +1,133 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using XSharpModel;
+
+namespace XSharp.ProjectSystem.LanguageService
+{
+ ///
+ /// Converts the Xsc command line arguments of the design-time build (rule XSharpCompilerCommandLineArgs)
+ /// into the inputs of the code model: the option list for ,
+ /// the assembly references for and the source files.
+ ///
+ ///
+ /// XParseOptions.FromVsValues expects the switches without the leading '/' and the defines as "d:",
+ /// the same format that XSharpProjectOptions.BuildCommandLine (MPFproj) produces.
+ /// Switches that are not X# parse options are ignored by FromVsValues.
+ ///
+ internal sealed class XSharpCommandLine
+ {
+ public IList ParseOptions { get; }
+ public IList References { get; }
+ public IList SourceFiles { get; }
+
+ private XSharpCommandLine(IList parseOptions, IList references, IList sourceFiles)
+ {
+ ParseOptions = parseOptions;
+ References = references;
+ SourceFiles = sourceFiles;
+ }
+
+ public static XSharpCommandLine Parse(IEnumerable arguments, string projectFolder)
+ {
+ var options = new List();
+ var references = new List();
+ var sources = new List();
+ var includes = new List();
+ foreach (var argument in arguments)
+ {
+ if (string.IsNullOrWhiteSpace(argument))
+ continue;
+ var arg = argument.Trim();
+ if (arg[0] != '/' && arg[0] != '-')
+ {
+ sources.Add(MakeFullPath(Unquote(arg), projectFolder));
+ continue;
+ }
+ arg = arg.Substring(1);
+ string name, value;
+ var pos = arg.IndexOf(':');
+ if (pos > 0)
+ {
+ name = arg.Substring(0, pos);
+ value = Unquote(arg.Substring(pos + 1));
+ }
+ else
+ {
+ name = arg;
+ value = "";
+ }
+ switch (name.ToLowerInvariant())
+ {
+ case "reference":
+ case "r":
+ var reference = StripAlias(value);
+ if (reference.Length > 0)
+ references.Add(MakeFullPath(reference, projectFolder));
+ break;
+ case "define":
+ case "d":
+ if (value.Length > 0)
+ options.Add("d:" + value);
+ break;
+ case "i":
+ if (value.Length > 0)
+ includes.Add(value);
+ break;
+ case "analyzer":
+ case "analyzerconfig":
+ case "additionalfile":
+ // paths for the Roslyn analyzers, not relevant for the code model
+ break;
+ default:
+ options.Add(arg);
+ break;
+ }
+ }
+ // Like MPFproj: the default include folder is always part of the include path
+ includes.Add(XParseOptions.DefaultIncludeDir);
+ options.Add("i:" + string.Join(";", includes.Where(i => !string.IsNullOrEmpty(i))));
+ return new XSharpCommandLine(options, references, sources);
+ }
+
+ private static string Unquote(string value)
+ {
+ value = value.Trim();
+ if (value.Length >= 2 && value[0] == '"' && value[value.Length - 1] == '"')
+ value = value.Substring(1, value.Length - 2);
+ return value;
+ }
+
+ ///
+ /// /reference:alias=path.dll -> path.dll
+ ///
+ private static string StripAlias(string value)
+ {
+ var pos = value.IndexOf('=');
+ if (pos > 0 && value.IndexOfAny(new[] { '\\', '/', ':' }) > pos)
+ value = value.Substring(pos + 1);
+ return Unquote(value);
+ }
+
+ internal static string MakeFullPath(string path, string projectFolder)
+ {
+ try
+ {
+ if (!Path.IsPathRooted(path))
+ path = Path.Combine(projectFolder, path);
+ return Path.GetFullPath(path);
+ }
+ catch (Exception)
+ {
+ return path;
+ }
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/LanguageService/XSharpProjectAdapter.cs b/src/VisualStudio/ProjectSystemCPS/LanguageService/XSharpProjectAdapter.cs
new file mode 100644
index 0000000000..c77c6c0939
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/LanguageService/XSharpProjectAdapter.cs
@@ -0,0 +1,549 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Threading.Tasks.Dataflow;
+using Microsoft.VisualStudio.ProjectSystem;
+using Microsoft.VisualStudio.ProjectSystem.Properties;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Threading;
+using XSharp.Settings;
+using XSharpModel;
+
+namespace XSharp.ProjectSystem.LanguageService
+{
+ ///
+ /// Feeds the X# code model ( ) of a project that is loaded by the CPS project system.
+ /// It replaces the data feeding that XSharpProjectNode (MPFproj) performs: hierarchy walking and the parsing
+ /// of the response file after a real build (spec WP-5, Part B).
+ ///
+ ///
+ /// One instance per UnconfiguredProject. The data comes from the active configuration
+ /// (IActiveConfiguredProjectSubscriptionService), so there is one XProject per project, as with MPFproj.
+ /// Three subscriptions:
+ ///
+ /// - source items (evaluation) -> XProject.AddFile / RemoveFile
+ /// - XSharpProjectProperties + EvaluatedProjectReference (evaluation) -> RootNamespace, output paths,
+ /// provisional parse options (dialect) and X# project references
+ /// - XSharpCompilerCommandLineArgs (design-time build) -> XParseOptions and assembly references
+ ///
+ /// Because the design-time build does not run the compiler, parse options and references are available
+ /// before the first real build.
+ ///
+ [Export(ExportContractNames.Scopes.UnconfiguredProject, typeof(IProjectDynamicLoadComponent))]
+ [AppliesTo(XSharpCapabilities.XSharpCps)]
+ internal sealed class XSharpProjectAdapter : IProjectDynamicLoadComponent, IXSharpProject
+ {
+ private const string ProjectPropertiesRule = "XSharpProjectProperties";
+ private const string ProjectReferenceRule = "EvaluatedProjectReference";
+ private const string CommandLineRule = "XSharpCompilerCommandLineArgs";
+
+ ///
+ /// Item types whose items are registered with the code model. XProject.AddFile determines the file type
+ /// from the extension, non source files end up in the "other files" of the project, as with MPFproj.
+ ///
+ private static readonly HashSet FileItemTypes = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Compile", "None", "Content", "EmbeddedResource", "Page", "ApplicationDefinition", "Resource", "VOBinary", "NativeResource",
+ };
+
+ private readonly UnconfiguredProject project;
+ private readonly IActiveConfiguredProjectSubscriptionService subscriptions;
+ private readonly IProjectThreadingService threading;
+ private readonly IntellisenseErrorStore errors = new IntellisenseErrorStore();
+ private readonly HashSet files = new HashSet(StringComparer.OrdinalIgnoreCase);
+ private readonly HashSet projectReferences = new HashSet(StringComparer.OrdinalIgnoreCase);
+ // Two locks, always taken in this order (modelGate, then gate):
+ // - modelGate serializes the changes of the code model (the three subscriptions, AddFileNode/DeleteFileNode,
+ // load/unload); XProject.AddFile/RemoveFile do database I/O.
+ // - gate only protects the file set and the model field, briefly, so HasFileNode (called by the VO designers on
+ // the UI thread) never waits for that I/O.
+ private readonly object modelGate = new object();
+ private readonly object gate = new object();
+
+ private XProject model;
+ private List links;
+ private IntellisenseErrorList errorList;
+ private CommentTaskList taskList;
+ private SourceFileWatcher sourceWatcher;
+ private XParseOptions parseOptions = XParseOptions.Default;
+ private bool parseOptionsFromCommandLine;
+ private string rootNamespace = "";
+ private string intermediateOutputPath = "";
+ private string outputFile = "";
+ private bool prefixClassesWithDefaultNamespace;
+
+ [ImportingConstructor]
+ public XSharpProjectAdapter(
+ UnconfiguredProject project,
+ IActiveConfiguredProjectSubscriptionService subscriptions,
+ IProjectThreadingService threading)
+ {
+ this.project = project;
+ this.subscriptions = subscriptions;
+ this.threading = threading;
+ }
+
+ internal XProject Model => model;
+
+ private string ProjectFolder => Path.GetDirectoryName(project.FullPath);
+
+ #region IProjectDynamicLoadComponent
+
+ public async Task LoadAsync()
+ {
+ if (!await EnsureSolutionIsOpenAsync())
+ {
+ XSettings.Information("XSharpProjectAdapter: X# solution model is not open, no code model for " + project.FullPath);
+ return;
+ }
+ // Create the code model on the UI thread, like MPFproj: the projects of a solution load in parallel, and
+ // creating two XProjects at the same time on background threads raced in the code model (the lazily
+ // created OrphanedFiles project was added twice: SQLite "FOREIGN KEY constraint failed" with a new
+ // X# database, verified in VS).
+ await threading.JoinableTaskFactory.SwitchToMainThreadAsync();
+ // Before the first walk, so the parser finds the comment tasks (TODO etc.)
+ CommentTokens.EnsureSet();
+ lock (modelGate)
+ {
+ if (model != null)
+ return;
+ var newModel = new XProject(this);
+ newModel.ProjectWalkComplete += OnProjectWalkComplete;
+ newModel.FileWalkComplete += OnFileWalkComplete;
+ lock (gate)
+ {
+ model = newModel;
+ }
+ }
+ XSettings.Information("XSharpProjectAdapter: created code model for " + project.FullPath);
+ // Forms added with "Add New Item" open in the shadow designer instead of the code editor
+ ShadowDesigner.NewFormDesignerRedirect.EnsureAdvised(threading.JoinableTaskFactory);
+ // Source files that change on disk (editor saves, VO designers, external tools) are walked again, like
+ // XSharpProjectNode.OnFileChanged
+ sourceWatcher = new SourceFileWatcher(ProjectFolder, IsSourceFileOfProject, WalkChangedFile, WalkProject);
+ errorList = new IntellisenseErrorList(threading.JoinableTaskFactory, DisplayName, errors.GetAll);
+ errors.Changed = errorList.Update;
+ taskList = new CommentTaskList(threading.JoinableTaskFactory, DisplayName);
+
+ var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };
+ links = new List
+ {
+ // The source item rules are dynamic (one per item type): take the rule names from
+ // SourceItemRuleNamesSource. LinkTo without rule names subscribes to no rules at all.
+ subscriptions.SourceItemsRuleSource.SourceBlock.LinkTo(
+ DataflowBlockSlim.CreateActionBlock>(OnSourceItemsChanged),
+ subscriptions.SourceItemRuleNamesSource.SourceBlock,
+ linkOptions),
+ subscriptions.ProjectRuleSource.SourceBlock.LinkTo(
+ DataflowBlockSlim.CreateActionBlock>(OnEvaluationChanged),
+ linkOptions,
+ ruleNames: new[] { ProjectPropertiesRule, ProjectReferenceRule }),
+ subscriptions.ProjectBuildRuleSource.SourceBlock.LinkTo(
+ DataflowBlockSlim.CreateActionBlock>(OnDesignTimeBuildChanged),
+ linkOptions,
+ ruleNames: new[] { CommandLineRule }),
+ };
+ }
+
+ public Task UnloadAsync()
+ {
+ XProject oldModel;
+ lock (modelGate)
+ {
+ if (links != null)
+ {
+ foreach (var link in links)
+ link.Dispose();
+ links = null;
+ }
+ if (model != null)
+ {
+ model.ProjectWalkComplete -= OnProjectWalkComplete;
+ model.FileWalkComplete -= OnFileWalkComplete;
+ }
+ oldModel = model;
+ lock (gate)
+ {
+ model = null;
+ files.Clear();
+ }
+ errors.Changed = null;
+ sourceWatcher?.Dispose();
+ sourceWatcher = null;
+ errorList?.Dispose();
+ errorList = null;
+ taskList?.Dispose();
+ taskList = null;
+ projectReferences.Clear();
+ }
+ oldModel?.Close();
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Makes sure that the X# solution model (database) is open before the code model is created.
+ ///
+ ///
+ /// The X# project package opens it in OnBeforeOpenSolution. For solutions with the MPFproj project type
+ /// VS loads that package before the solution opens (it provides the project factory). For the CPS project
+ /// type it does not: the X# package is only loaded later (autoload), misses OnBeforeOpenSolution and does
+ /// not see the solution yet while it is still opening, so the database was never opened.
+ /// Load the X# project package (shell link, logging, settings) and open the database for the solution
+ /// file that is being loaded, like XSharpShellLink does in OnBeforeOpenSolution.
+ /// The solution file does not have to exist yet: a project that is opened without a solution
+ /// (devenv project.xsproj, File > Open > Project) or a new project lives in a solution that VS only writes
+ /// when it is saved. XSolution.Open only needs the folder and the name (database in .vs\{name}), so the
+ /// database is opened for the future solution file; without a solution file name the project file is used.
+ ///
+ private async Task EnsureSolutionIsOpenAsync()
+ {
+ if (XSolution.IsOpen)
+ return true;
+ await threading.JoinableTaskFactory.SwitchToMainThreadAsync();
+ if (ServiceProvider.GlobalProvider.GetService(typeof(SVsShell)) is IVsShell shell)
+ {
+ var packageGuid = new Guid(XSharpConstants.guidXSharpProjectPkgString);
+ if (shell is IVsShell7 shell7)
+ await shell7.LoadPackageAsync(ref packageGuid);
+ else
+ shell.LoadPackage(ref packageGuid, out _);
+ await threading.JoinableTaskFactory.SwitchToMainThreadAsync();
+ }
+ if (!XSolution.IsOpen)
+ {
+ string solutionFile = null;
+ if (ServiceProvider.GlobalProvider.GetService(typeof(SVsSolution)) is IVsSolution solution &&
+ solution.GetSolutionInfo(out _, out var file, out _) == 0)
+ {
+ solutionFile = file;
+ }
+ if (string.IsNullOrEmpty(solutionFile))
+ solutionFile = Path.ChangeExtension(project.FullPath, ".sln");
+ XSettings.Information("XSharpProjectAdapter: opening the X# solution model for " + solutionFile +
+ (File.Exists(solutionFile) ? "" : " (solution file not saved yet)"));
+ XSolution.Open(solutionFile);
+ }
+ return XSolution.IsOpen;
+ }
+
+ #endregion
+
+ #region Subscriptions
+
+ private void OnSourceItemsChanged(IProjectVersionedValue update)
+ {
+ try
+ {
+ var added = new List();
+ var removed = new List();
+ lock (modelGate)
+ {
+ var current = model;
+ if (current == null)
+ return;
+ foreach (var change in update.Value.ProjectChanges)
+ {
+ if (!FileItemTypes.Contains(change.Key) || !change.Value.Difference.AnyChanges)
+ continue;
+ var diff = change.Value.Difference;
+ foreach (var item in diff.RemovedItems)
+ removed.Add(XSharpCommandLine.MakeFullPath(item, ProjectFolder));
+ foreach (var item in diff.AddedItems)
+ added.Add(XSharpCommandLine.MakeFullPath(item, ProjectFolder));
+ foreach (var rename in diff.RenamedItems)
+ {
+ removed.Add(XSharpCommandLine.MakeFullPath(rename.Key, ProjectFolder));
+ added.Add(XSharpCommandLine.MakeFullPath(rename.Value, ProjectFolder));
+ }
+ }
+ List toRemove, toAdd;
+ lock (gate)
+ {
+ toRemove = removed.Where(files.Remove).ToList();
+ toAdd = added.Where(files.Add).ToList();
+ }
+ // Database I/O, outside gate
+ foreach (var file in toRemove)
+ current.RemoveFile(file);
+ foreach (var file in toAdd)
+ current.AddFile(file);
+ }
+ if (added.Count > 0 || removed.Count > 0)
+ {
+ XSettings.Information($"XSharpProjectAdapter: {project.FullPath}: {added.Count} files added, {removed.Count} files removed");
+ WalkProject();
+ }
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ }
+ }
+
+ private void OnEvaluationChanged(IProjectVersionedValue update)
+ {
+ try
+ {
+ var changes = update.Value.ProjectChanges;
+ lock (modelGate)
+ {
+ if (model == null)
+ return;
+ if (changes.TryGetValue(ProjectPropertiesRule, out var properties) && properties.Difference.AnyChanges)
+ {
+ ReadProjectProperties(properties.After);
+ XSettings.Information($"XSharpProjectAdapter: {project.FullPath}: properties RootNamespace={rootNamespace}, TargetPath={outputFile}, NS={prefixClassesWithDefaultNamespace}");
+ }
+ if (changes.TryGetValue(ProjectReferenceRule, out var references) && references.Difference.AnyChanges)
+ {
+ UpdateProjectReferences(references.After);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ }
+ }
+
+ private void OnDesignTimeBuildChanged(IProjectVersionedValue update)
+ {
+ try
+ {
+ if (!update.Value.ProjectChanges.TryGetValue(CommandLineRule, out var change) || !change.Difference.AnyChanges)
+ return;
+ var arguments = change.After.Items.Keys.ToList();
+ if (arguments.Count == 0)
+ {
+ // A failed design-time build returns no arguments. Keep the previous options and references.
+ return;
+ }
+ var commandLine = XSharpCommandLine.Parse(arguments, ProjectFolder);
+ var options = XParseOptions.FromVsValues(commandLine.ParseOptions);
+ XProject current;
+ lock (modelGate)
+ {
+ current = model;
+ if (current == null)
+ return;
+ parseOptions = options;
+ parseOptionsFromCommandLine = true;
+ current.ResetParseOptions(options);
+ current.RefreshReferences(commandLine.References);
+ foreach (var reference in projectReferences)
+ current.AddProjectReference(reference);
+ }
+ // Loads the referenced assemblies: outside the locks. XProject guards it itself (_resolvingReferences),
+ // and the ModelWalker and the type lookups call it without these locks as well.
+ current.ResolveReferences();
+ XSettings.Information($"XSharpProjectAdapter: {project.FullPath}: design-time build, {commandLine.ParseOptions.Count} options, {commandLine.References.Count} references, dialect {options.Dialect}");
+ WalkProject();
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ }
+ }
+
+ private void ReadProjectProperties(IProjectRuleSnapshot snapshot)
+ {
+ rootNamespace = GetProperty(snapshot, "RootNamespace");
+ intermediateOutputPath = GetProperty(snapshot, "IntermediateOutputPath");
+ if (intermediateOutputPath.Length > 0)
+ intermediateOutputPath = XSharpCommandLine.MakeFullPath(intermediateOutputPath, ProjectFolder);
+ outputFile = GetProperty(snapshot, "TargetPath");
+ prefixClassesWithDefaultNamespace = string.Equals(GetProperty(snapshot, "NS"), "true", StringComparison.OrdinalIgnoreCase);
+ if (!parseOptionsFromCommandLine)
+ {
+ // Provisional options until the first design-time build has returned the command line
+ var dialect = GetProperty(snapshot, "Dialect");
+ var options = new List { "dialect:" + (dialect.Length > 0 ? dialect : "Core"), "i:" + XParseOptions.DefaultIncludeDir };
+ parseOptions = XParseOptions.FromVsValues(options);
+ model.ResetParseOptions(parseOptions);
+ }
+ }
+
+ private void UpdateProjectReferences(IProjectRuleSnapshot snapshot)
+ {
+ // X# -> X# project references: the code model reads the types from the referenced XProject (source).
+ // References to other projects arrive as /reference: to their output assembly in the command line.
+ var current = new HashSet(
+ snapshot.Items.Keys
+ .Select(item => XSharpCommandLine.MakeFullPath(item, ProjectFolder))
+ .Where(XProject.IsXSharpProject),
+ StringComparer.OrdinalIgnoreCase);
+ foreach (var removed in projectReferences.Where(r => !current.Contains(r)).ToList())
+ {
+ projectReferences.Remove(removed);
+ model.RemoveProjectReference(removed);
+ }
+ foreach (var added in current.Where(r => !projectReferences.Contains(r)).ToList())
+ {
+ projectReferences.Add(added);
+ model.AddProjectReference(added);
+ }
+ }
+
+ private static string GetProperty(IProjectRuleSnapshot snapshot, string name)
+ {
+ return snapshot.Properties.TryGetValue(name, out var value) && value != null ? value : "";
+ }
+
+ // Like XSharpProjectNode.OnProjectWalkComplete/OnFileWalkComplete: refresh the comment tasks
+ private void OnProjectWalkComplete(XProject xProject) => RefreshCommentTasks();
+
+ private void OnFileWalkComplete(XFile xFile) => RefreshCommentTasks();
+
+ private void RefreshCommentTasks()
+ {
+ try
+ {
+ var current = model;
+ var tasks = taskList;
+ if (current != null && tasks != null)
+ tasks.Update(current.GetCommentTasks());
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ }
+ }
+
+ ///
+ /// For (its thread): is a source file of this project.
+ ///
+ private bool IsSourceFileOfProject(string path)
+ {
+ XProject current;
+ lock (gate)
+ {
+ current = model;
+ if (current == null || !files.Contains(path))
+ return false;
+ }
+ return current.FindXFile(path)?.IsSource == true;
+ }
+
+ ///
+ /// For (background thread): walk a changed source file again. notify: true
+ /// raises FileWalkComplete, which refreshes the Task List.
+ ///
+ private void WalkChangedFile(string path)
+ {
+ var current = model;
+ var file = current?.FindXFile(path);
+ if (file == null || !file.IsSource)
+ return;
+ XSettings.Information("XSharpProjectAdapter: " + path + " changed on disk, walking it again");
+ current.WalkFile(file, true);
+ }
+
+ private void WalkProject()
+ {
+ var current = model;
+ if (current != null)
+ ModelWalker.AddProject(current);
+ }
+
+ #endregion
+
+ #region IXSharpProject
+
+ public string DisplayName => Path.GetFileNameWithoutExtension(project.FullPath);
+ public string IntermediateOutputPath => intermediateOutputPath;
+ public string OutputFile => outputFile;
+ public XParseOptions ParseOptions => parseOptions;
+ public bool PrefixClassesWithDefaultNamespace => prefixClassesWithDefaultNamespace;
+ public string RootNameSpace => rootNamespace;
+ public string Url => project.FullPath;
+
+ public bool HasFileNode(string fileName)
+ {
+ if (string.IsNullOrEmpty(fileName))
+ return false;
+ var fullPath = XSharpCommandLine.MakeFullPath(fileName, ProjectFolder);
+ lock (gate)
+ {
+ return files.Contains(fullPath);
+ }
+ }
+
+ ///
+ /// Called by the VO designers (XSharpVoEditors, Functions.EnsureFileNodeExists) after they have written a
+ /// generated file (.prg, .rc, ...) to disk.
+ ///
+ ///
+ /// In SDK-style X# projects these files are included by the globs of XSharp.SDK.Props (including
+ /// DependentUpon), so the project file does not change: CPS picks the file up with the next evaluation.
+ /// The file is registered with the code model immediately, so HasFileNode is true right away; the later
+ /// source item update finds it already registered.
+ /// Projects without default items (EnableDefaultItems=false, legacy format) would need an explicit item
+ /// through CPS; that belongs to the legacy-format step.
+ ///
+ public void AddFileNode(string fileName)
+ {
+ if (string.IsNullOrEmpty(fileName))
+ return;
+ var fullPath = XSharpCommandLine.MakeFullPath(fileName, ProjectFolder);
+ lock (modelGate)
+ {
+ XProject current;
+ lock (gate)
+ {
+ current = model;
+ if (current == null || !files.Add(fullPath))
+ return;
+ }
+ current.AddFile(fullPath);
+ }
+ }
+
+ ///
+ /// Called by the VO designers before they delete a generated file. See .
+ ///
+ public void DeleteFileNode(string fileName)
+ {
+ if (string.IsNullOrEmpty(fileName))
+ return;
+ var fullPath = XSharpCommandLine.MakeFullPath(fileName, ProjectFolder);
+ lock (modelGate)
+ {
+ XProject current;
+ lock (gate)
+ {
+ current = model;
+ if (current == null || !files.Remove(fullPath))
+ return;
+ }
+ current.RemoveFile(fullPath);
+ }
+ }
+
+ public void ClearIntellisenseErrors(string fileName) => errors.Clear(fileName);
+
+ ///
+ /// The code model errors and the errors and warnings of the last build (squiggles, XSharpErrorColorizer),
+ /// like XSharpProjectNode.GetIntellisenseErrors in MPFproj.
+ ///
+ public List GetIntellisenseErrors(string filename) => errors.Get(filename);
+ public void AddIntellisenseError(XError error) => errors.Add(error);
+
+ #endregion
+
+ ///
+ /// Called by after a build of .
+ ///
+ internal void SetBuildErrors(string configuration, IEnumerable positions) =>
+ errors.SetBuildErrors(configuration, positions);
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/ProjectSystemCPS.csproj b/src/VisualStudio/ProjectSystemCPS/ProjectSystemCPS.csproj
new file mode 100644
index 0000000000..218a32e309
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/ProjectSystemCPS.csproj
@@ -0,0 +1,77 @@
+
+
+
+
+
+ net48
+ XSharp.ProjectSystemCPS
+ XSharp.ProjectSystem
+ AnyCPU
+
+ false
+ false
+ false
+
+ true
+ true
+ $(NoWarn);NU1903
+
+
+
+ true
+ true
+ false
+ false
+
+
+ TRACE;DEBUG;XSHARP;DEV17;XSHARP_PROJECTSYSTEMCPS
+ full
+
+
+ TRACE;XSHARP;DEV17;XSHARP_PROJECTSYSTEMCPS
+ pdbonly
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+ $(VsExternalDir)XSharp.Vsparser.dll
+ false
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/VisualStudio/ProjectSystemCPS/Properties/AssemblyInfo.cs b/src/VisualStudio/ProjectSystemCPS/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..9369eab5d4
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+using System.Reflection;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+
+[assembly: AssemblyTitle("XSharp.ProjectSystemCPS")]
+[assembly: AssemblyDescription("X# project system based on the Visual Studio Common Project System (CPS)")]
diff --git a/src/VisualStudio/ProjectSystemCPS/PropertyPages/KeyValuePairListEncoding.cs b/src/VisualStudio/ProjectSystemCPS/PropertyPages/KeyValuePairListEncoding.cs
new file mode 100644
index 0000000000..d7ad1091da
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/PropertyPages/KeyValuePairListEncoding.cs
@@ -0,0 +1,72 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+using System.Collections.Generic;
+using System.Linq;
+
+namespace XSharp.ProjectSystem.PropertyPages
+{
+ ///
+ /// The value format of the property editors' name/value lists (MultiStringSelector, NameValueList):
+ /// "name1=value1,name2=value2", with '/' escaping '/', ',' and '='.
+ ///
+ ///
+ /// The same format as KeyValuePairListEncoding of the managed project system, which is internal to it.
+ ///
+ internal static class KeyValuePairListEncoding
+ {
+ public static IEnumerable<(string Name, string Value)> Parse(string input)
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ yield break;
+ foreach (var entry in ReadEntries(input))
+ {
+ var (encodedName, encodedValue) = SplitEntry(entry);
+ var name = Decode(encodedName);
+ if (!string.IsNullOrEmpty(name))
+ yield return (name, Decode(encodedValue));
+ }
+ }
+
+ public static string Format(IEnumerable<(string Name, string Value)> pairs) =>
+ string.Join(",", pairs.Select(pair => Encode(pair.Name) + "=" + Encode(pair.Value)));
+
+ private static string Encode(string value) => value.Replace("/", "//").Replace(",", "/,").Replace("=", "/=");
+
+ private static string Decode(string value) => value.Replace("/=", "=").Replace("/,", ",").Replace("//", "/");
+
+ private static IEnumerable ReadEntries(string text)
+ {
+ bool escaped = false;
+ int start = 0;
+ for (int i = 0; i < text.Length; i++)
+ {
+ if (text[i] == ',' && !escaped)
+ {
+ yield return text.Substring(start, i - start);
+ start = i + 1;
+ escaped = false;
+ }
+ else
+ {
+ escaped = text[i] == '/' && !escaped;
+ }
+ }
+ yield return text.Substring(start);
+ }
+
+ private static (string Name, string Value) SplitEntry(string entry)
+ {
+ bool escaped = false;
+ for (int i = 0; i < entry.Length; i++)
+ {
+ if (entry[i] == '=' && !escaped)
+ return (entry.Substring(0, i), entry.Substring(i + 1));
+ escaped = entry[i] == '/' && !escaped;
+ }
+ return (string.Empty, string.Empty);
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/PropertyPages/XSharpDefineConstantsPropertiesProvider.cs b/src/VisualStudio/ProjectSystemCPS/PropertyPages/XSharpDefineConstantsPropertiesProvider.cs
new file mode 100644
index 0000000000..df15ddced3
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/PropertyPages/XSharpDefineConstantsPropertiesProvider.cs
@@ -0,0 +1,180 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Build.Execution;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Framework.XamlTypes;
+using Microsoft.VisualStudio.ProjectSystem;
+using Microsoft.VisualStudio.ProjectSystem.Properties;
+using Microsoft.VisualStudio.Threading;
+
+namespace XSharp.ProjectSystem.PropertyPages
+{
+ ///
+ /// Persistence "XSharpDefineConstants" of the "Conditional compilation symbols" field (DefineConstants in
+ /// BuildPropertyPage.XSharp.xaml): the project file, with the value converted like C# does.
+ ///
+ ///
+ /// The managed Build page edits DefineConstants with a MultiStringSelector, whose value is a list of name/value
+ /// pairs ("MYSYM=False"). For C# the managed DefineConstantsValueProvider (persistence
+ /// "ProjectFileWithInterception", AppliesTo CSharp | FSharp) converts it: the list shows only the symbols of the
+ /// project file after "$(DefineConstants);", and a change is written as "$(DefineConstants);MYSYM", so the
+ /// implicit symbols (DEBUG, TRACE) are kept. For X# nothing converted it: VS wrote "MYSYM=False" into the project
+ /// file and DEBUG and TRACE were lost (verified in VS). That value provider cannot be used for X#: its interfaces
+ /// are in Microsoft.VisualStudio.ProjectSystem.Managed.dll, which has no package and a version per VS release.
+ /// So this provider does the same with the public CPS API: it wraps the project file provider and converts only
+ /// DefineConstants.
+ ///
+ // The property pages import the unnamed contracts and select the provider by the "Name" metadata
+ // (PropertyPagesDataModelProvider); the named exports are for imports by persistence name.
+ [Export(PersistenceName, typeof(IProjectPropertiesProvider))]
+ [Export(typeof(IProjectPropertiesProvider))]
+ [Export(PersistenceName, typeof(IProjectInstancePropertiesProvider))]
+ [Export(typeof(IProjectInstancePropertiesProvider))]
+ [ExportMetadata("Name", PersistenceName)]
+ [AppliesTo(XSharpCapabilities.XSharpCps)]
+ internal sealed class XSharpDefineConstantsPropertiesProvider : IProjectPropertiesProvider, IProjectInstancePropertiesProvider
+ {
+ public const string PersistenceName = "XSharpDefineConstants";
+ private const string ProjectFileContract = "Microsoft.VisualStudio.ProjectSystem.ProjectFile";
+
+ private readonly IProjectPropertiesProvider provider;
+ private readonly IProjectInstancePropertiesProvider instanceProvider;
+ private readonly UnconfiguredProject project;
+
+ [ImportingConstructor]
+ public XSharpDefineConstantsPropertiesProvider(
+ [Import(ProjectFileContract)] IProjectPropertiesProvider provider,
+ [Import(ProjectFileContract)] IProjectInstancePropertiesProvider instanceProvider,
+ UnconfiguredProject project)
+ {
+ this.provider = provider;
+ this.instanceProvider = instanceProvider;
+ this.project = project;
+ }
+
+ public string DefaultProjectPath => project.FullPath;
+
+ public event AsyncEventHandler ProjectPropertyChanged
+ {
+ add => provider.ProjectPropertyChanged += value;
+ remove => provider.ProjectPropertyChanged -= value;
+ }
+
+ public event AsyncEventHandler ProjectPropertyChangedOnWriter
+ {
+ add => provider.ProjectPropertyChangedOnWriter += value;
+ remove => provider.ProjectPropertyChangedOnWriter -= value;
+ }
+
+ public event AsyncEventHandler ProjectPropertyChanging
+ {
+ add => provider.ProjectPropertyChanging += value;
+ remove => provider.ProjectPropertyChanging -= value;
+ }
+
+ // The property pages read and write through GetProperties; the rest is passed on unchanged
+ public IProjectProperties GetProperties(string file, string itemType, string item) =>
+ new DefineConstantsProperties(provider.GetProperties(file, itemType, item));
+
+ public IProjectProperties GetCommonProperties() => provider.GetCommonProperties();
+ public IProjectProperties GetItemProperties(string itemType, string item) => provider.GetItemProperties(itemType, item);
+ public IProjectProperties GetItemTypeProperties(string itemType) => provider.GetItemTypeProperties(itemType);
+ public IProjectProperties GetCommonProperties(ProjectInstance projectInstance) => instanceProvider.GetCommonProperties(projectInstance);
+ public IProjectProperties GetItemTypeProperties(ProjectInstance projectInstance, string itemType) => instanceProvider.GetItemTypeProperties(projectInstance, itemType);
+ public IProjectProperties GetItemProperties(ProjectInstance projectInstance, string itemType, string itemName) => instanceProvider.GetItemProperties(projectInstance, itemType, itemName);
+ public IProjectProperties GetItemProperties(ITaskItem taskItem) => instanceProvider.GetItemProperties(taskItem);
+
+ ///
+ /// The project file properties; DefineConstants is converted between the MultiStringSelector list and
+ /// "$(DefineConstants);SYMBOL;..." (DefineConstantsValueProvider of the managed project system).
+ ///
+ private sealed class DefineConstantsProperties : IProjectProperties, IRuleAwareProjectProperties
+ {
+ private const string DefineConstants = nameof(DefineConstants);
+ private const string RecursivePrefix = "$(DefineConstants)";
+
+ private readonly IProjectProperties properties;
+
+ public DefineConstantsProperties(IProjectProperties properties)
+ {
+ this.properties = properties;
+ }
+
+ private static bool IsDefineConstants(string propertyName) =>
+ string.Equals(propertyName, DefineConstants, StringComparison.OrdinalIgnoreCase);
+
+ public IProjectPropertiesContext Context => properties.Context;
+ public string FileFullPath => properties.FileFullPath;
+ public PropertyKind PropertyKind => properties.PropertyKind;
+
+ public Task DeleteDirectPropertiesAsync() => properties.DeleteDirectPropertiesAsync();
+ public Task DeletePropertyAsync(string propertyName, IReadOnlyDictionary dimensionalConditions = null) =>
+ properties.DeletePropertyAsync(propertyName, dimensionalConditions);
+ public Task> GetDirectPropertyNamesAsync() => properties.GetDirectPropertyNamesAsync();
+ public Task> GetPropertyNamesAsync() => properties.GetPropertyNamesAsync();
+ public Task GetEvaluatedPropertyValueAsync(string propertyName) => properties.GetEvaluatedPropertyValueAsync(propertyName);
+ public Task IsValueInheritedAsync(string propertyName) => properties.IsValueInheritedAsync(propertyName);
+
+ public async Task GetUnevaluatedPropertyValueAsync(string propertyName)
+ {
+ var value = await properties.GetUnevaluatedPropertyValueAsync(propertyName);
+ if (!IsDefineConstants(propertyName))
+ return value;
+ // Only the symbols of the project file itself; the imported ones (DEBUG, TRACE) are shown as the
+ // evaluated preview
+ if (value == null || await properties.IsValueInheritedAsync(propertyName))
+ return string.Empty;
+ return KeyValuePairListEncoding.Format(ParseOwnSymbols(value).Select(symbol => (symbol, bool.FalseString)));
+ }
+
+ public async Task SetPropertyValueAsync(string propertyName, string unevaluatedPropertyValue, IReadOnlyDictionary dimensionalConditions = null)
+ {
+ if (!IsDefineConstants(propertyName))
+ {
+ await properties.SetPropertyValueAsync(propertyName, unevaluatedPropertyValue, dimensionalConditions);
+ return;
+ }
+ // Exactly the symbols of the list, after the inherited ones. (The managed provider also leaves out
+ // symbols of an outer "$(DefineConstants);..." definition; a symbol defined twice does no harm.)
+ var symbols = KeyValuePairListEncoding.Parse(unevaluatedPropertyValue)
+ .Select(pair => pair.Name.Trim())
+ .Where(symbol => symbol.Length > 0)
+ .Distinct(StringComparer.Ordinal)
+ .ToList();
+ if (symbols.Count == 0)
+ {
+ await properties.DeletePropertyAsync(propertyName, dimensionalConditions);
+ return;
+ }
+ await properties.SetPropertyValueAsync(propertyName, RecursivePrefix + ";" + string.Join(";", symbols), dimensionalConditions);
+ }
+
+ ///
+ /// The symbols after "$(DefineConstants);". A value without that prefix (written by hand or by MPFproj)
+ /// is taken as a whole, so its symbols are shown and kept.
+ ///
+ private static IEnumerable ParseOwnSymbols(string value)
+ {
+ value = value?.Trim() ?? string.Empty;
+ if (value.StartsWith(RecursivePrefix, StringComparison.OrdinalIgnoreCase))
+ value = value.Substring(RecursivePrefix.Length);
+ return value.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(symbol => symbol.Trim())
+ .Where(symbol => symbol.Length > 0);
+ }
+
+ public void SetRuleContext(Rule rule)
+ {
+ (properties as IRuleAwareProjectProperties)?.SetRuleContext(rule);
+ }
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/PropertyPages/XSharpProjectDesignerPageProvider.cs b/src/VisualStudio/ProjectSystemCPS/PropertyPages/XSharpProjectDesignerPageProvider.cs
new file mode 100644
index 0000000000..9118112337
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/PropertyPages/XSharpProjectDesignerPageProvider.cs
@@ -0,0 +1,41 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.ProjectSystem;
+using Microsoft.VisualStudio.ProjectSystem.VS.Properties;
+
+namespace XSharp.ProjectSystem.PropertyPages
+{
+ ///
+ /// Makes the project designer (Project > Properties, double click on the Properties folder) available for
+ /// X# CPS projects.
+ ///
+ ///
+ /// CPS only offers the project designer when at least one IVsProjectDesignerPageProvider applies to the
+ /// project (VsProjectDesignerPageService.IsProjectDesignerSupported). The managed project system only has
+ /// providers for C#, VB and F#.
+ /// Because X# CPS projects have the ProjectPropertiesEditor capability, the AppDesigner editor factory opens
+ /// the new property editor, which shows the XAML rules with Context "Project" (the managed pages plus the X#
+ /// pages from XSharpBuildTask\Rules). The classic COM property pages listed by this provider are not used
+ /// in that case, so there are none: the X# COM property pages (AppDesigner2022) depend on MPFproj.
+ ///
+ [Export(typeof(IVsProjectDesignerPageProvider))]
+ [AppliesTo(XSharpCapabilities.XSharpCps + " & AppDesigner")]
+ internal sealed class XSharpProjectDesignerPageProvider : IVsProjectDesignerPageProvider
+ {
+ private static readonly Task> noPages =
+ Task.FromResult>(Array.Empty());
+
+ public Task> GetPagesAsync()
+ {
+ return noPages;
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/README.md b/src/VisualStudio/ProjectSystemCPS/README.md
new file mode 100644
index 0000000000..25a9e349e6
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/README.md
@@ -0,0 +1,519 @@
+# X# project system for SDK-style projects (CPS)
+
+`XSharp.ProjectSystemCPS` loads **SDK-style X# projects** (``) in Visual Studio with the
+**Common Project System (CPS)**, the project system that Visual Studio also uses for SDK-style C#, VB and F#
+projects. Legacy (non-SDK) X# projects keep using the classic X# project system, a fork of MPFproj
+(`ProjectBase` and `ProjectPackage`). Both run side by side; a project selector decides per project.
+
+The X# language service and code model (`XSharpCodeModelXs`) are unchanged. This assembly only replaces the part
+that feeds them: instead of walking the MPFproj hierarchy and reading a response file, it takes the project items,
+references and compiler options from CPS and its design-time builds.
+
+Contents:
+
+1. [What you get](#1-what-you-get)
+2. [Supported environments](#2-supported-environments)
+3. [Installation](#3-installation)
+4. [How it works](#4-how-it-works)
+5. [Source layout](#5-source-layout)
+6. [Building](#6-building)
+7. [Testing and debugging](#7-testing-and-debugging)
+8. [Troubleshooting](#8-troubleshooting)
+9. [Known limitations and open issues](#9-known-limitations-and-open-issues)
+10. [References](#10-references)
+
+---
+
+## 1. What you get
+
+For SDK-style X# projects, Visual Studio offers what it offers for SDK-style C# projects, plus the X# specific
+parts:
+
+- **Solution Explorer:** the Dependencies node (packages, assemblies, projects, COM, frameworks), file globbing,
+ "Edit Project File", nesting of dependent files (`.designer.prg`, `.rc`, VO binaries), X# icons for the project,
+ source files, forms, user controls and VO binaries.
+- **NuGet:** `PackageReference`, restore, "Manage NuGet Packages".
+- **Properties:** the new project properties editor with the managed pages (Application, Build, Build Events,
+ Package, Debug, …) and the X# pages Language and Dialect.
+- **Debugging:** launch profiles (`Properties/launchSettings.json`) and the framework dropdown of multi-target
+ projects.
+- **Publish and Pack** (`.nupkg`), as for C# projects.
+- **IntelliSense without a prior build:** completion, navigation and the other features of the X# language service
+ work as soon as the design-time build has run. A real build is not needed.
+- **Error squiggles** for build errors, and **Task List** entries for comment tokens (`// TODO` …).
+- **Designers:** the VO designers (window, menu, DBServer, FieldSpec) and the WinForms designer through the
+ *shadow designer*; "Add .designer file" for forms written in a single file.
+
+---
+
+## 2. Supported environments
+
+| Environment | SDK-style X# projects | Legacy X# projects |
+|---|---|---|
+| Visual Studio 2022 (17.x) | CPS (this assembly) | MPFproj |
+| Visual Studio 2026 (18.x) | CPS (tested with 18.10) | MPFproj |
+| Visual Studio 2019 (16.x) | not supported ("cannot be loaded inside this version of Visual Studio", as before) | MPFproj |
+
+`XSharp.ProjectSystemCPS` only uses the public CPS SDK (`Microsoft.VisualStudio.ProjectSystem.Sdk` 17.9). It does
+not reference the private assemblies of the managed project system (`Microsoft.VisualStudio.ProjectSystem.Managed*.dll`),
+because they have no package and change their version with every Visual Studio release.
+
+---
+
+## 3. Installation
+
+### 3.1 What has to be installed
+
+The CPS project system needs two parts, which **must come from the same build**:
+
+1. **The VSIX** (`XSharp.Project2022.vsix`, built by `ProjectPackage2022`). It contains `XSharp.ProjectSystemCPS.dll`,
+ its pkgdef (package, project type, project selector) and `XSharp.ProjectSystemCPS.imagemanifest` (icons).
+2. **The X# MSBuild support files** in the X# `MsBuild` folder (`XSharpBuildTask`, installed by the X# setup):
+
+ | File | Purpose |
+ |---|---|
+ | `XSharp.CurrentVersion.targets`, `XSharp.CrossTargeting.targets` | import `XSharp.DesignTime.targets` when the project is loaded by CPS |
+ | `XSharp.DesignTime.targets` | the CPS design-time integration (managed design-time targets, capabilities, rules) |
+ | `XSharp.SDK.Props` | item globs of SDK-style projects; excludes the shadow designer companion folders |
+ | `Rules\ProjectItemsSchema.XSharp.xaml` | file extensions → item types (Compile, None, NativeResource, VOBinary) |
+ | `Rules\XSharpCompilerCommandLineArgs.xaml`, `Rules\XSharpProjectProperties.xaml` | design-time data for the code model |
+ | `Rules\NativeResource*.xaml`, `Rules\VOBinary*.xaml` | properties of `.rc` files and VO binaries |
+ | `Rules\ApplicationPropertyPage.XSharp.xaml`, `BuildPropertyPage.XSharp.xaml`, `LanguagePage.XSharp.xaml`, `DialectPage.XSharp.xaml`, `ReferencesPage.XSharp.xaml` | the property pages |
+
+ `XSharp.Build.csproj` copies the rules to its output folder (`CopyToOutputDirectory`); **the X# installer must ship
+ the `Rules` folder** together with the targets.
+
+### 3.2 Finding the MSBuild support files
+
+The project selector checks the support files before it sends a project to CPS
+(`Selection/XSharpMsBuildSupport.cs`). It looks for the X# `MsBuild` folder like `XSharp.BeforeCommon.Props` does:
+
+1. the environment variable `XSharpMsBuildDir`,
+2. the registry value `XSharpPath` (`HKLM\SOFTWARE\WOW6432Node\XSharpBV\XSharp` or `HKLM\SOFTWARE\XSharpBV\XSharp`)
+ plus `MsBuild`,
+3. `%ProgramFiles(x86)%\XSharp\MsBuild`.
+
+CPS is used when `XSharp.CurrentVersion.targets` there contains the `XSharpCpsProjectSystem` import and
+`Rules\XSharpCompilerCommandLineArgs.xaml` exists.
+
+### 3.3 Mismatched installations
+
+- **VSIX newer than the X# installation** (support files without the CPS import): SDK-style projects are not loaded.
+ The selector sends them to MPFproj, which shows "The project file … is an SDK style project. SDK style X# projects
+ are loaded by the X# project system for SDK style projects, which needs the X# MSBuild support files of a matching
+ X# installation. Please run the X# setup program again." Legacy projects are not affected.
+- **X# installation newer than the VSIX:** the targets only import the design-time integration when the global
+ property `XSharpCpsProjectSystem` is `true`, which only this project system sets. Command-line builds and MPFproj
+ projects are unchanged.
+
+### 3.4 Assemblies in the GAC
+
+The X# setup installs `XSharp.CodeDomProvider`, `XSharp.VSParser` and `XSharp.Evaluator` in the GAC. Visual Studio
+always loads the GAC copy when its identity matches, also for development builds of the VSIX. A change to one of these
+assemblies (for example the CodeDOM generator used by "Add .designer file" and the designers) only becomes effective
+in Visual Studio with a matching X# installation.
+
+---
+
+## 4. How it works
+
+### 4.1 Project selection
+
+Solutions reference X# projects with the MPFproj project type GUID `{AA6C8D78-22FF-423A-9C7C-5F2393824E04}`. The CPS
+project type has its own GUID `{AB494DCE-A11B-4583-851E-D4A86775B3F0}`.
+
+`Selection/XSharpProjectSelector` is an `IVsProjectSelector` (the mechanism the managed project system uses for
+`FSharpProjectSelector`), registered for **both** project types (`Selection/ProvideProjectSelectorAttribute`,
+`Projects\{type}\ProjectSelector`). For every X# project that is opened it decides:
+
+- **CPS** when the project file is SDK-style (``, `` or ``,
+ `Selection/SdkProjectFile`) **and** the MSBuild support files support CPS (3.2);
+- **MPFproj** otherwise, also when the file cannot be read.
+
+The package `XSharpProjectSystemPackage` registers the selector object with `IVsRegisterProjectSelector` when it
+initializes; Visual Studio loads the package on demand.
+
+When Visual Studio saves a solution it writes the GUID of the project system that actually loaded the project, so the
+entry of an SDK-style project changes once from `{AA6C8D78…}` to `{AB494DCE…}` (C# SDK projects do the same:
+`{FAE04EC0…}` → `{9A19103F…}`; Visual Studio has no way to prevent it). Because the selector is registered for the CPS
+GUID as well, such an entry still goes to MPFproj when the project is legacy or the support files are too old.
+
+The X# project templates still use the MPFproj project type (template language `XSharp`); SDK-style templates reach
+CPS through the selector. The CPS project type has its own template language `XSharpCps`, so no template is offered
+twice.
+
+### 4.2 Project type and capabilities
+
+`XSharpProjectSystemPackage` registers the CPS project type (`[ProjectTypeRegistration]`) with the initial
+capabilities of `XSharpCapabilities.ProjectTypeCapabilities`:
+
+```
+XSharpCps; AppDesigner; HandlesOwnReload; OpenProjectFile; PreserveFormatting;
+ProjectConfigurationsDeclaredDimensions; ProjectPropertiesEditor; .NET; UseProjectEvaluationCache
+```
+
+This is the set of the managed C# project type without `LanguageService` (X# has its own language service, not the
+Roslyn workspace), `CSharp`, `SharedImports` and `EditAndContinue`. `XSharpCps` is the gate for all MEF parts of this
+assembly (`[AppliesTo("XSharpCps")]`).
+
+`XSharp.DesignTime.targets` adds the MSBuild capabilities of `Microsoft.Managed.DesignTime.targets`
+(`DependenciesTree`, `PackageReferences`, `AssemblyReferences`, `ProjectReferences`, `COMReferences`, `LaunchProfiles`,
+`Publish`, `UseFileGlobs`, …) plus `XSharp; Managed; SingleTargetBuildForStartupProjects; ProjectPropertiesEditor`, and
+removes `LanguageService` and `ReferencesFolder`.
+
+### 4.3 MSBuild integration
+
+- `Evaluation/XSharpCpsGlobalPropertiesProvider` sets the **global property `XSharpCpsProjectSystem=true`** for projects
+ of the CPS project type.
+- `XSharp.CurrentVersion.targets` and `XSharp.CrossTargeting.targets` import `XSharp.DesignTime.targets` only when that
+ property is `true` and the files exist (`$(XSharpDesignTimeTargetsPath)`, `$(XSharpManagedDesignTimeTargetsPath)`,
+ both overridable). MPFproj projects and command-line builds are therefore not affected.
+- `XSharp.DesignTime.targets` imports the managed design-time targets from the Visual Studio installation (like F#),
+ sets the X# capabilities and rules, and points `AddItemTemplatesGuid`, `GeneratorsTypeGuid` and `CmdUIContextGuid`
+ to the MPFproj project factory, where the X# package already registers the item templates and single-file
+ generators. **Add New Item** therefore offers the X# templates.
+- `XSharpCpsProjectSystem` is appended to `_GlobalPropertiesToRemoveFromProjectReferences`, so referenced projects
+ (C#, legacy X#) are built with their own global properties and are not built twice.
+- `Rules/ProjectItemsSchema.XSharp.xaml` maps the file extensions: `.prg/.prgx/.xs` → `Compile`, `.vh/.xh/.ch` → `None`,
+ `.rc` → `NativeResource`, `.xsfrm/.vnfrm/.xsdbs/.vndbs/.xsmnu/.vnmnu/.xsfs/.vnfs/.xssql/.vnsqs/.xsrep/.vnrep` → `VOBinary`.
+ The globs themselves come from the .NET SDK and `XSharp.SDK.Props` (with `DependentUpon` for designer files, `.rc`
+ files and VO binaries).
+
+### 4.4 Feeding the code model
+
+`LanguageService/XSharpProjectAdapter` is the core. It is an `IProjectDynamicLoadComponent` (one per project,
+`UnconfiguredProject` scope) and implements `IXSharpProject`, the interface through which the code model talks to a
+project system. When a project loads it:
+
+1. makes sure the X# solution model (`XSolution`) is open. If the X# package has not opened it yet (a pure CPS
+ solution, a project opened without a `.sln`), the adapter loads the X# package and opens the model for the solution
+ file, also when the solution has not been saved yet (`.vs\{name}\X#Model.xsdb`);
+2. creates the `XProject` **on the UI thread**, like MPFproj (parallel creation raced on the X# database);
+3. subscribes to the **active configuration** through `IActiveConfiguredProjectSubscriptionService`:
+
+ | Subscription | Data | Code model |
+ |---|---|---|
+ | `SourceItemsRuleSource` (rule names from `SourceItemRuleNamesSource`) | Compile, None, Content, EmbeddedResource, Page, ApplicationDefinition, Resource, VOBinary, NativeResource items | `AddFile` / `RemoveFile` (a rename is remove + add) |
+ | `ProjectRuleSource` (`XSharpProjectProperties`, `EvaluatedProjectReference`) | RootNamespace, IntermediateOutputPath, TargetPath, NS, project references | properties; provisional parse options (dialect only) until the first design-time build; `AddProjectReference` / `RemoveProjectReference` |
+ | `ProjectBuildRuleSource` (`XSharpCompilerCommandLineArgs`) | the compiler command line of the design-time build (`CompileDesignTime`) | `XParseOptions.FromVsValues`, `RefreshReferences`, `ResolveReferences` |
+
+ `XSharpCommandLine` converts the Xsc switches into the format of `FromVsValues` (the same result as the old
+ response-file path). A failed design-time build (empty result) keeps the previous state.
+4. starts the model walker (`ModelWalker.AddProject`) after file changes and design-time builds.
+
+Two locks keep this consistent: `modelGate` serializes all changes of the code model, `gate` only protects the file set
+(so `HasFileNode`, called by the designers on the UI thread, never waits for database I/O).
+
+`LanguageService/SourceFileWatcher` (a `FileSystemWatcher` on the project folder) walks source files of the project
+that change on disk 500 ms after the last change: saves in the editor, files written by the designers, external tools,
+source control. After a buffer overflow the whole project is walked again. Linked files outside the project folder are
+not watched.
+
+`AddFileNode` / `DeleteFileNode` (called by the VO designers after they wrote or before they delete a generated file)
+register the file with the code model immediately; the project file does not change, because the SDK globs include the
+files.
+
+### 4.5 Errors and the Task List
+
+- `LanguageService/BuildErrorLoggerProvider` (`IBuildLoggerProviderAsync`) attaches an MSBuild logger to real builds
+ (Build, Rebuild, Clean; not design-time builds) and passes the errors and warnings to the adapter
+ (`SetBuildErrors`, per configuration). `IntellisenseErrorStore` merges them with the code model errors; the X#
+ error tagger draws the **squiggles** from them. Build errors are not added to the Error List again, Visual Studio
+ already shows them.
+- `LanguageService/IntellisenseErrorList` shows code model errors in the Error List. The code model currently reports
+ none (as with MPFproj), so the list stays empty.
+- `LanguageService/CommentTaskList` shows the comment tasks of the project in the Task List after each walk.
+ `CommentTokens` passes the Task List tokens (Tools > Options > Environment > Task List) to the code model when no
+ MPFproj project did.
+
+### 4.6 Property pages
+
+The project properties are the new properties editor of Visual Studio (capability `ProjectPropertiesEditor`) with XAML
+rules. `PropertyPages/XSharpProjectDesignerPageProvider` is needed so that Visual Studio offers the project designer at
+all (it only has providers for C#/VB/F#).
+
+| Page | Rule | Content |
+|---|---|---|
+| Application | `ApplicationPropertyPage.XSharp.xaml` (extends the managed page) | manifest, `VulcanCompatibleResources`, `UseNativeVersion`, `NoWin32Manifest`; the C# Win32 resource choice is hidden (X# compiles the `.rc` files instead) |
+| Build | `BuildPropertyPage.XSharp.xaml` (extends the managed page) | other flags (`CommandLineOption`), shared compiler, warning level, `SuppressRCWarnings`, conditional compilation symbols; C#-only options hidden |
+| Language | `LanguagePage.XSharp.xaml` | dialect, named arguments, unsafe, case sensitivity, overflow checks, include paths, standard defines, … |
+| Dialect | `DialectPage.XSharp.xaml` | VO1–VO17, LB, XPP1, FOX2, FOX3 (depending on the dialect) |
+| Global Usings | `ReferencesPage.XSharp.xaml` | `ImplicitUsings` |
+| Build Events, Debug, Package, … | managed pages | |
+
+**Conditional compilation symbols:** the managed Build page edits `DefineConstants` with a list editor whose value is a
+list of name/value pairs. For C# a value provider of the managed project system converts it; for X# nothing did, so the
+editor wrote `MYSYM=False` into the project file and lost the implicit `DEBUG;TRACE`.
+`BuildPropertyPage.XSharp.xaml` therefore defines `DefineConstants` with its own persistence `XSharpDefineConstants`,
+implemented by `PropertyPages/XSharpDefineConstantsPropertiesProvider`: it wraps the project file provider, shows only
+the project's own symbols (after `$(DefineConstants);`), writes `$(DefineConstants);MYSYM` and deletes the property
+when the list is empty. `PropertyPages/KeyValuePairListEncoding` is the list format of the editors.
+
+`` items (global usings with alias or static) are edited in the project file; the C# "Manage Implicit Global
+Usings" editor needs a value provider that only exists for C#.
+
+Changing a property triggers a design-time build; the adapter picks up the new parse options from it.
+
+### 4.7 Solution Explorer icons
+
+`Imaging/XSharpProjectTreePropertiesProvider` sets the X# icons (`XSharp.ProjectSystemCPS.imagemanifest`): the project,
+`.prg/.prgx/.xs` files, VO binaries, and forms and user controls. A `.prg` is a form or user control when its `SubType`
+says so or, since SDK-style projects rarely have one, when a matching `.Designer.prg` exists and its class inherits
+from `Form`/`UserControl` (also over a line continuation, the format of the CodeDOM generator).
+
+CPS only recalculates the icons of nodes that changed in a tree update. Adding a `.Designer.prg` below an existing
+`.prg` does not change the `.prg` node, so `Imaging/XSharpProjectTreePropertiesProviderSource`
+(`IProjectTreePropertiesProviderDataSource`) supplies the provider and publishes a new instance when a `.Designer.prg`
+is added or removed; CPS then recalculates all nodes. Its values carry their own version key: with the versions of the
+source items subscription the tree waited for ever.
+
+The image library resolves the manifest's pack URIs at startup, so the package has `[ProvideBindingPath]` and
+`ProjectPackage2022` a `ProvideCodeBase` for `XSharp.ProjectSystemCPS`.
+
+### 4.8 VO designers
+
+The VO window, menu, DBServer and FieldSpec editors are registered by file extension in the X# package and do not
+depend on the project system. They get the project through `XSolution.FindFile(file).Project.ProjectNode`, which is the
+adapter for files of CPS projects. They write the generated `.prg`/`.rc` files to disk and call
+`HasFileNode`/`AddFileNode`/`DeleteFileNode`; `SourceFileWatcher` makes the code model read the regenerated files.
+
+### 4.9 WinForms designer (shadow designer)
+
+The Visual Studio WinForms designer only works with C# and VB projects that the managed project system loaded (for
+.NET it runs out of process and needs the project's references, target framework and Roslyn workspace). The shadow
+designer therefore opens X# forms through a generated **companion C# project**:
+
+1. **Companion:** `ShadowDesignerBridge` converts the form (`Form1.prg` plus `Form1.Designer.prg`) with the X# CodeDOM
+ parser and the C# code provider into `Form1.Designer.cs` and a stub `Form1.cs` (same base class and imports), in a
+ companion project `.ShadowDesigner\.ShadowDesigner.csproj` next to the project folder
+ (`CompanionProjectWriter`). The companion gets the target framework of the loaded project and its resolved
+ references. `XSharp.SDK.Props` excludes `**/*.ShadowDesigner/**` from X# projects.
+2. **Solution:** `SolutionWiring` adds the companion to the solution folder "Shadow Designer (generated)" and opens
+ `Form1.Designer.cs` in the designer.
+3. **Saving:** `CompanionSaveWatcher` and `DesignerChangesSync` write the designer's changes back as X#
+ (`Form1.Designer.prg`) with the X# CodeDOM generator; `EventHandlerSync` adds event handlers to `Form1.prg`.
+ The context menu commands "Sync Designer Changes From Shadow Designer" and "Sync Event Handlers From Shadow Designer"
+ do the same on demand.
+4. **Cleanup:** `ShadowDesignerCleanup` removes the companion projects and their folders when the solution closes
+ (with retries while the designer host still locks a folder, and cancellation when the companion is used again).
+
+Entry points in CPS projects: double click / Enter (`XSharpShadowDesignerDefaultActionCommand`) and View Designer
+(`XSharpShadowDesignerViewFormCommand`) on a `.prg` that has a `.Designer.prg`. Forms created with **Add New Item**
+open in the designer too: the template wizard opens them in the code editor, so `NewFormDesignerRedirect` switches a
+form created within the last 30 s to the designer.
+
+**References:** the designer needs the assemblies of the project's packages. When they are not known yet (the solution
+has just been opened, the NuGet restore is still running), `ShadowDesignerBridge.EnsureReferencesAsync` waits for the
+IntelliSense stage of the solution load and then up to 20 s for the design-time build, with a status bar message. If
+they are still missing it offers a build ("Build now?"; Tools > Options > X# Custom Editors > Other Editors,
+"Auto-build for Shadow Designer", builds without asking) and waits up to 3 s after it. CPS runs command handlers while
+it waits synchronously on the UI thread, so in that case the command returns at once and the designer opens in the
+background; if it cannot be opened, the form opens in the code editor.
+
+### 4.10 Commands of the X# package
+
+Most commands of `ProjectPackage/Commands` are global (XPorters, UDC tester, repository window, …) and work unchanged.
+Project-specific ones:
+
+- **Add .designer file** (context menu of a form `.prg`): splits a form written in a single file into `Form.prg` and
+ `Form.designer.prg`. For CPS projects the form is recognized through the code model (base classes of the first
+ class), the CodeDOM provider works on the path, and the new file needs no project item (globs). The form is parsed
+ first; when the project is still loading and the form class cannot be recognized, a message asks to try again.
+- **Convert to Windows Form**, **Sync …** commands: work for CPS projects.
+- **Edit project file**: only for MPFproj projects; CPS has its own "Edit Project File".
+
+### 4.11 Multi-targeting
+
+Projects with `` build and debug all target frameworks (framework dropdown, launch profiles). The
+code model follows the **active configuration**, which CPS sets to the first entry of ``; the
+framework dropdown only selects what F5 starts. IntelliSense therefore uses the references and defines of the first
+target framework. This matches the default editor context of C#, which in addition offers a context dropdown in the
+editor (one Roslyn project per target framework); the X# code model has one context per project.
+
+---
+
+## 5. Source layout
+
+```
+src/VisualStudio/ProjectSystemCPS/
+ XSharpProjectSystemPackage.cs package, CPS project type, project selector registration
+ XSharpCapabilities.cs capability names and the initial capabilities
+ Evaluation/
+ XSharpCpsGlobalPropertiesProvider.cs global property XSharpCpsProjectSystem=true
+ Selection/
+ XSharpProjectSelector.cs IVsProjectSelector: SDK-style → CPS, else MPFproj
+ ProvideProjectSelectorAttribute.cs pkgdef registration of the selector
+ SdkProjectFile.cs "is this an SDK-style project file"
+ XSharpMsBuildSupport.cs do the installed MSBuild support files support CPS
+ LanguageService/
+ XSharpProjectAdapter.cs IXSharpProject: feeds XProject from CPS
+ XSharpCommandLine.cs Xsc switches → XParseOptions.FromVsValues
+ SourceFileWatcher.cs walks source files changed on disk
+ BuildErrorLoggerProvider.cs build errors for the squiggles
+ IntellisenseErrorStore.cs code model and build errors per file
+ IntellisenseErrorList.cs Error List provider
+ CommentTaskList.cs, CommentTokens.cs Task List
+ PropertyPages/
+ XSharpProjectDesignerPageProvider.cs enables the project designer
+ XSharpDefineConstantsPropertiesProvider.cs persistence "XSharpDefineConstants"
+ KeyValuePairListEncoding.cs list format of the property editors
+ Imaging/
+ XSharpImages.cs image monikers
+ XSharpProjectTreePropertiesProvider.cs icons
+ XSharpProjectTreePropertiesProviderSource.cs recalculates the icons when designer files change
+ ShadowDesigner/ WinForms designer through a companion C# project (4.9)
+ XSharp.ProjectSystemCPS.imagemanifest, Resources/*.png
+
+src/Compiler/src/Compiler/XSharpBuildTask/
+ XSharp.DesignTime.targets CPS design-time integration
+ XSharp.CurrentVersion.targets, XSharp.CrossTargeting.targets gated import
+ XSharp.SDK.Props SDK item globs
+ Rules/*.xaml items schema, design-time data, property pages
+
+src/VisualStudio/ProjectSystemCPS.TestBed/ test projects and StartExp.cmd (7.1)
+```
+
+Other parts involved:
+
+- `src/VisualStudio/XSharpCodeModelXs` — the code model (`XProject`, `XSolution`, `IXSharpProject` in `Interfaces.prg`),
+ unchanged.
+- `src/VisualStudio/ProjectPackage` — the X# package (language service registration, editors, commands, MPFproj
+ project type). `ProjectPackage2022` references `ProjectSystemCPS` and ships it in its VSIX.
+ `XSharpProjectFactory.CreateProject` rejects SDK-style projects that reach MPFproj.
+- `src/VisualStudio/ProjectBase` — the MPFproj fork, for legacy projects only.
+- `src/VisualStudio/CodeDomProvider` — the X# CodeDOM provider (designers, "Add .designer file").
+
+Rules for changes:
+
+- `ProjectSystemCPS` references `XSharpCodeModelXs` (and `CodeDomProvider`), **never** `ProjectBase` or `ProjectPackage`;
+ the dependency goes the other way.
+- Only the public CPS SDK; no reference to `Microsoft.VisualStudio.ProjectSystem.Managed*.dll`.
+- Keep the public API of `XSharpCodeModelXs` unchanged.
+
+---
+
+## 6. Building
+
+`ProjectSystemCPS` is part of `src/VSIntegration2022.sln` and is built with it (`src/buildvs2022.cmd Debug|Release`).
+It is an SDK-style `net48` project; `ProjectPackage2022` puts the assembly, its pkgdef and the image manifest into its
+VSIX.
+
+To build the VSIX alone (from `src/VisualStudio`, with the Visual Studio 2022 MSBuild):
+
+```
+msbuild ProjectPackage/ProjectPackage2022.csproj -p:Configuration=Debug -p:Platform=AnyCPU
+```
+
+- Pass `Platform=AnyCPU` (without a space); other values leave the `DEV17` define unset and give misleading errors.
+- A Debug build **deploys** the VSIX into the experimental instance of Visual Studio 2022; the instance must be
+ closed. `-p:DeployTargetInstanceId=` deploys into another Visual Studio installation instead, for
+ example Visual Studio 2026 (the instance id from `vswhere -all`).
+- The Debug build of `ProjectPackage2022` overwrites the Visual Studio 2019 manifest
+ `ProjectPackage/source.extension.vsixmanifest`; restore it (`git checkout`) before committing.
+- The MSBuild support files (3.1) are built by `XSharp.Build.csproj` (`src/buildcompiler.cmd`).
+
+---
+
+## 7. Testing and debugging
+
+### 7.1 Test bed
+
+`src/VisualStudio/ProjectSystemCPS.TestBed`:
+
+| Project / solution | Purpose |
+|---|---|
+| `CpsTestBed.xsproj`, `CpsTestBed.sln` | SDK-style console project (Core dialect, net48); the solution references it with the CPS GUID |
+| `Legacy/LegacyTestBed.xsproj` | legacy project, must stay on MPFproj |
+| `WinForms/WinFormsTestBed.xsproj` | SDK-style WinForms project (net8.0-windows) for the shadow designer |
+| `VO/VOTestBed.xsproj` | SDK-style copy of the VO SDI Application template (net48, x86) for the VO designers |
+| `SelectorTestBed.sln` | all four projects with the MPFproj GUID: tests the selector |
+
+`StartExp.cmd [solution]` starts the Visual Studio 2022 experimental instance for tests with the MSBuild support files
+of the repository **without changing the installed X#**: it copies the installed X# `MsBuild` folder to
+`Artifacts\CpsMsBuild`, copies the changed repository files over it (`XSharp.CurrentVersion.targets`,
+`XSharp.CrossTargeting.targets`, `XSharp.DesignTime.targets`, `XSharp.SDK.Props`, `Rules`), deletes the image library
+cache of the experimental instance and starts `devenv /rootSuffix Exp` with `XSharpMsBuildDir` pointing to the copy.
+Default solution: `CpsTestBed.sln`. For Visual Studio 2026, start its `devenv.exe /rootSuffix Exp ` the same
+way with `XSharpMsBuildDir` set to `Artifacts\CpsMsBuild`.
+
+After a test, reset the test bed: Visual Studio rewrites the project type GUIDs in `SelectorTestBed.sln` (4.1), and the
+designers rewrite the files of `WinForms` and `VO`.
+
+For a "never built" state, delete `bin`, `Artifacts\obj\` and the `.vs` folder of the test bed (the X#
+database `X#Model.xsdb`).
+
+### 7.2 Logs
+
+- **X# log:** `%TEMP%\XSharp.Intellisense\Project_*.log`, one file per Visual Studio session. Useful entries:
+ `XSharpProjectSelector: -> CPS|MPFproj`, `XSharpMsBuildSupport: …`,
+ `XSharpProjectAdapter: … design-time build, N options, M references, dialect …`, `… files added, … files removed`,
+ `… changed on disk, walking it again`, `ShadowDesignerBridge: …`, `ShadowDesignerCleanup: …`.
+- **ActivityLog:** `%APPDATA%\Microsoft\VisualStudio\\ActivityLog.xml` (UTF-16). Package load failures
+ (`SetSite failed for package [XSharpProjectSystemPackage]`) are logged even without `/log`. Pass the solution before
+ `/log` (`devenv /log`), otherwise Visual Studio writes the log into the solution file.
+- **MEF errors:** `%LOCALAPPDATA%\Microsoft\VisualStudio\\ComponentModelCache\Microsoft.VisualStudio.Default.err`.
+- **Binlogs of Visual Studio builds:** start Visual Studio with `MSBUILDDEBUGENGINE=1` and `MSBUILDDEBUGPATH=`;
+ every build and design-time build writes a binlog there.
+
+### 7.3 Useful techniques
+
+- **Hangs:** attach non-invasively and dump the managed stacks:
+ `cdb -pv -p -c ".loadby sos clr; !EEStack -EE; qd"`. A UI thread in CPS `ProjectNode.HrInvoke` with no thread
+ working on the project tree means that the tree waits for a data source.
+- **Assembly resolution:** Visual Studio resolves extension and platform assemblies through the per-instance
+ `%LOCALAPPDATA%\Microsoft\VisualStudio\\devenv.exe.config`, generated from the pkgdef `RuntimeConfiguration`
+ entries. The merged configuration is in `privateregistry.bin` of the instance; it can be read without admin rights
+ with `RegLoadAppKey` while Visual Studio is closed.
+- **NuGet scenarios:** a `PackageReference` with `RestoreSources` (a local folder feed) and `RestorePackagesPath` in the
+ project makes the restore controllable (delete the packages folder and `project.assets.json`: restore at load; rename
+ the feed: the restore fails). Keep the paths short: NuGet inside Visual Studio does not extract files beyond 260
+ characters.
+- **GAC'd assemblies** (3.4) can be tested outside Visual Studio with a small console program that has
+ ` ` in its `.config` and `DEVPATH` pointing to the build output.
+
+---
+
+## 8. Troubleshooting
+
+| Symptom | Cause and remedy |
+|---|---|
+| "The project file … is an SDK style project … Please run the X# setup program again." | The selector sent the project to MPFproj. Either the MSBuild support files are too old (install the matching X#; the X# log shows `XSharpMsBuildSupport: … does not support CPS`), or the CPS package did not load (ActivityLog: `SetSite failed for package [XSharpProjectSystemPackage]`). |
+| `SetSite failed … Could not load file or assembly 'Microsoft.VisualStudio.ProjectSystem.VS, Version=17.0.0.0'` in an experimental instance | The configuration of that instance lacks the CPS `codeBase` entries (seen with an instance created before a Visual Studio update). Reset it: `CreateExpInstance.exe /Reset /VSInstance=_ /RootSuffix=Exp` (`VSSDK\VisualStudioIntegration\Tools\Bin`), then deploy again. This deletes the settings and extensions of that experimental instance. |
+| No X# icons | The image library cache does not contain the X# manifest. Delete `ImageLibrary\ImageLibrary.cache` of the instance (`StartExp.cmd` does this) and restart. |
+| No IntelliSense, completion empty | Check the X# log for the design-time build line of the project. If it is missing, check the Output window (Build) and the binlog of the design-time build; if the adapter reports 0 references, the design-time build failed. |
+| "Could not find the form class …" from "Add .designer file" | The project is still loading and the references are not known yet; try again when completion works in the file. |
+| The shadow designer shows "… is not contained within a project that supports code" | The companion project did not pick up the file; close the designer and open it again. Check the X# log (`ShadowDesignerBridge`). |
+| The `.sln` changes the project type GUID of SDK-style projects | Expected, once (4.1). |
+| "One or more projects in the solution were not loaded correctly" after Visual Studio was killed | The Output window (Solution) reports that the document restore is skipped once. Harmless. |
+
+---
+
+## 9. Known limitations and open issues
+
+- **Multi-targeting:** IntelliSense uses the first target framework (4.11). A possible improvement is to follow the
+ framework selected in the dropdown.
+- **"Include files" folder:** the virtual folder of MPFproj that lists `#include` files does not exist for CPS projects.
+- **Default items turned off:** in SDK-style projects with `EnableDefaultItems=false` (or
+ `EnableDefaultCompileItems=false`), `.prg` files generated by the VO designers do not become part of the project,
+ because the adapter relies on the globs. The X# globs of `XSharp.SDK.Props` (VO binaries, `.rc`, `*.designer.prg`,
+ `*.resx`, `*.xaml.prg`) ignore these properties.
+- **Shadow designer saves:** writing back rebuilds `Form.Designer.prg` from the fields and `InitializeComponent` only;
+ other members (`Dispose`), regions and field initializers of that file are lost (the same logic existed before for
+ the MPFproj SDK projects).
+- **Shadow designer companion** is visible in Solution Explorer and written into the `.sln` while the solution is open.
+- **Code model errors** are not reported by the code model, so the Error List shows only build errors (as with MPFproj).
+- **Global usings editor:** `` items are edited in the project file (4.6).
+- **Installation:** the VSIX and the MSBuild support files must match (3.3).
+- **Visual Studio 2019** cannot load SDK-style X# projects.
+
+---
+
+## 10. References
+
+- CPS: [microsoft/VSProjectSystem](https://github.com/microsoft/VSProjectSystem) (documentation of the Common Project
+ System and its extensibility points).
+- The managed project system (C#, VB, F#): [dotnet/project-system](https://github.com/dotnet/project-system), the
+ reference for capabilities, rules, design-time builds, project selectors (`FSharpProjectSelector`) and the language
+ service integration.
+- The code model contract: `src/VisualStudio/XSharpCodeModelXs/Interfaces.prg` (`IXSharpProject`) and the smallest
+ implementation, `OrphanedFilesProject.prg`.
diff --git a/src/VisualStudio/ProjectSystemCPS/Resources/XSharpDocument.png b/src/VisualStudio/ProjectSystemCPS/Resources/XSharpDocument.png
new file mode 100644
index 0000000000..90808762f3
Binary files /dev/null and b/src/VisualStudio/ProjectSystemCPS/Resources/XSharpDocument.png differ
diff --git a/src/VisualStudio/ProjectSystemCPS/Resources/XSharpProject.png b/src/VisualStudio/ProjectSystemCPS/Resources/XSharpProject.png
new file mode 100644
index 0000000000..c016270a13
Binary files /dev/null and b/src/VisualStudio/ProjectSystemCPS/Resources/XSharpProject.png differ
diff --git a/src/VisualStudio/ProjectSystemCPS/Selection/ProvideProjectSelectorAttribute.cs b/src/VisualStudio/ProjectSystemCPS/Selection/ProvideProjectSelectorAttribute.cs
new file mode 100644
index 0000000000..74dde8d968
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/Selection/ProvideProjectSelectorAttribute.cs
@@ -0,0 +1,64 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using Microsoft.VisualStudio.Shell;
+
+namespace XSharp.ProjectSystem.Selection
+{
+ ///
+ /// Registers a project selector for a project type:
+ ///
+ /// [$RootKey$\Projects\{projectType}] "ProjectSelector" = {selector}
+ /// [$RootKey$\ProjectSelectors\{selector}] "Package" = {package that registers the selector}
+ ///
+ /// VS loads the package when it needs the selector; the package registers the selector object with
+ /// IVsRegisterProjectSelector during its initialization.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
+ internal sealed class ProvideProjectSelectorAttribute : RegistrationAttribute
+ {
+ private readonly Guid projectTypeGuid;
+ private readonly Guid selectorGuid;
+
+ public ProvideProjectSelectorAttribute(string projectTypeGuid, string selectorGuid)
+ {
+ this.projectTypeGuid = new Guid(projectTypeGuid);
+ this.selectorGuid = new Guid(selectorGuid);
+ }
+
+ private string ProjectTypeKey => "Projects\\" + projectTypeGuid.ToString("B");
+ private string SelectorKey => "ProjectSelectors\\" + selectorGuid.ToString("B");
+
+ public override void Register(RegistrationContext context)
+ {
+ var projectKey = context.CreateKey(ProjectTypeKey);
+ try
+ {
+ projectKey.SetValue("ProjectSelector", selectorGuid.ToString("B"));
+ }
+ finally
+ {
+ projectKey.Close();
+ }
+ var selectorKey = context.CreateKey(SelectorKey);
+ try
+ {
+ selectorKey.SetValue("Package", context.ComponentType.GUID.ToString("B"));
+ }
+ finally
+ {
+ selectorKey.Close();
+ }
+ }
+
+ public override void Unregister(RegistrationContext context)
+ {
+ context.RemoveValue(ProjectTypeKey, "ProjectSelector");
+ context.RemoveKey(SelectorKey);
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/Selection/SdkProjectFile.cs b/src/VisualStudio/ProjectSystemCPS/Selection/SdkProjectFile.cs
new file mode 100644
index 0000000000..65365f0dc6
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/Selection/SdkProjectFile.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.XPath;
+
+namespace XSharp.ProjectSystem.Selection
+{
+ ///
+ /// Recognizes SDK-style project files. Used by and by the MPFproj project
+ /// factory, which rejects SDK-style projects.
+ ///
+ public static class SdkProjectFile
+ {
+ private const string MSBuildXmlNamespace = "http://schemas.microsoft.com/developer/msbuild/2003";
+
+ ///
+ /// True for SDK-style project files: <Project Sdk="...">, <Import Sdk="..." /> or <Sdk Name="..." />.
+ ///
+ public static bool IsSdkProject(string fileName)
+ {
+ if (string.IsNullOrEmpty(fileName))
+ return false;
+ XDocument document;
+ try
+ {
+ document = XDocument.Load(fileName);
+ }
+ catch (Exception)
+ {
+ // Not (yet) a valid project file, e.g. a new project that is still being created
+ return false;
+ }
+ return IsSdkProject(document);
+ }
+
+ internal static bool IsSdkProject(XDocument document)
+ {
+ var namespaces = new XmlNamespaceManager(new NameTable());
+ namespaces.AddNamespace("msb", MSBuildXmlNamespace);
+ return document.XPathSelectElement("/msb:Project[@Sdk]", namespaces) != null
+ || document.XPathSelectElement("/Project[@Sdk]") != null
+ || document.XPathSelectElement("/*/msb:Import[@Sdk]", namespaces) != null
+ || document.XPathSelectElement("/*/Import[@Sdk]") != null
+ || document.XPathSelectElement("/*/msb:Sdk[@Name]", namespaces) != null
+ || document.XPathSelectElement("/*/Sdk[@Name]") != null;
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/Selection/XSharpMsBuildSupport.cs b/src/VisualStudio/ProjectSystemCPS/Selection/XSharpMsBuildSupport.cs
new file mode 100644
index 0000000000..9673983bee
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/Selection/XSharpMsBuildSupport.cs
@@ -0,0 +1,76 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.IO;
+using Microsoft.Win32;
+using XSharp.Settings;
+
+namespace XSharp.ProjectSystem.Selection
+{
+ ///
+ /// Checks whether the installed X# MSBuild support files can be used by the CPS project system.
+ ///
+ ///
+ /// CPS projects need the design-time import in XSharp.CurrentVersion.targets (property XSharpCpsProjectSystem)
+ /// and the XAML rules in the Rules folder. These are part of the X# installation, not of the VSIX. When an older
+ /// X# installation is combined with this VSIX, SDK-style projects stay on MPFproj instead of loading in CPS
+ /// without rules.
+ /// The folder is determined like XSharp.BeforeCommon.Props does: environment variable XSharpMsBuildDir,
+ /// the XSharpPath in the registry, or Program Files (x86)\XSharp\MsBuild.
+ ///
+ internal static class XSharpMsBuildSupport
+ {
+ private static readonly Lazy supportsCps = new Lazy(CheckSupportsCps);
+
+ public static bool SupportsCps => supportsCps.Value;
+
+ private static bool CheckSupportsCps()
+ {
+ try
+ {
+ var folder = FindMsBuildFolder();
+ if (string.IsNullOrEmpty(folder))
+ {
+ XSettings.Information("XSharpMsBuildSupport: X# MsBuild folder not found, CPS disabled");
+ return false;
+ }
+ var targets = Path.Combine(folder, "XSharp.CurrentVersion.targets");
+ var rule = Path.Combine(folder, "Rules", "XSharpCompilerCommandLineArgs.xaml");
+ var supported = File.Exists(targets) && File.Exists(rule) &&
+ File.ReadAllText(targets).IndexOf("XSharpCpsProjectSystem", StringComparison.OrdinalIgnoreCase) >= 0;
+ XSettings.Information("XSharpMsBuildSupport: " + folder + (supported ? " supports CPS" : " does not support CPS (X# installation too old), SDK-style projects stay on the classic project system"));
+ return supported;
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ return false;
+ }
+ }
+
+ private static string FindMsBuildFolder()
+ {
+ var folder = Environment.GetEnvironmentVariable("XSharpMsBuildDir");
+ if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder))
+ return folder;
+ foreach (var key in new[] { @"SOFTWARE\WOW6432Node\XSharpBV\XSharp", @"SOFTWARE\XSharpBV\XSharp" })
+ {
+ using (var regKey = Registry.LocalMachine.OpenSubKey(key))
+ {
+ if (regKey?.GetValue("XSharpPath") is string path && !string.IsNullOrEmpty(path))
+ {
+ folder = Path.Combine(path, "MsBuild");
+ if (Directory.Exists(folder))
+ return folder;
+ }
+ }
+ }
+ folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "XSharp", "MsBuild");
+ return Directory.Exists(folder) ? folder : null;
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/Selection/XSharpProjectSelector.cs b/src/VisualStudio/ProjectSystemCPS/Selection/XSharpProjectSelector.cs
new file mode 100644
index 0000000000..394328dc46
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/Selection/XSharpProjectSelector.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Runtime.InteropServices;
+using Microsoft.VisualStudio.Shell.Interop;
+using XSharp.Settings;
+
+namespace XSharp.ProjectSystem.Selection
+{
+ ///
+ /// Chooses the project system for projects with the X# (MPFproj) project type GUID {AA6C8D78-...} or the CPS
+ /// project type GUID {AB494DCE-...}: SDK-style projects are loaded by the CPS project type, all other (legacy)
+ /// projects by MPFproj, which has no SDK-style support. VS rewrites {AA6C8D78} to {AB494DCE} in the .sln when it
+ /// saves the solution, so both GUIDs are routed the same way.
+ ///
+ ///
+ /// Same mechanism as the managed project system uses for C#/VB/F# (FSharpProjectSelector): the selector is
+ /// registered for the legacy project type GUID (pkgdef "Projects\{guid}\ProjectSelector", see
+ /// ), so existing solution files keep working. Unlike the managed
+ /// project system it is registered for the CPS project type GUID as well: VS persists the GUID of the factory
+ /// the selector returned, so an SDK project's .sln entry flips to {AB494DCE} once (C# has the same flip,
+ /// {FAE04EC0} -> {9A19103F}); with the second registration a legacy project behind such an entry still loads
+ /// with MPFproj.
+ /// SDK-style projects go to MPFproj only when the installed X# MSBuild support files lack the CPS design-time
+ /// import ( ) or the project file cannot be read; MPFproj then reports that
+ /// the project cannot be loaded (XSharpProjectFactory) instead of CPS failing with less clear errors.
+ ///
+ [Guid(XSharpConstants.guidProjectSelectorString)]
+ internal sealed class XSharpProjectSelector : IVsProjectSelector
+ {
+ public void GetProjectFactoryGuid(Guid guidProjectType, string pszFilename, out Guid guidProjectFactory)
+ {
+ guidProjectFactory = XSharpConstants.guidXSharpProjectFactory;
+ try
+ {
+ if (SdkProjectFile.IsSdkProject(pszFilename) && XSharpMsBuildSupport.SupportsCps)
+ {
+ guidProjectFactory = XSharpConstants.guidCpsProjectType;
+ }
+ }
+ catch (Exception e)
+ {
+ XSettings.Exception(e);
+ guidProjectFactory = XSharpConstants.guidXSharpProjectFactory;
+ }
+ XSettings.Information("XSharpProjectSelector: " + pszFilename + " -> " +
+ (guidProjectFactory == XSharpConstants.guidCpsProjectType ? "CPS" : "MPFproj"));
+ }
+ }
+}
diff --git a/src/VisualStudio/ProjectPackage/ShadowDesigner/CompanionProjectWriter.cs b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/CompanionProjectWriter.cs
similarity index 67%
rename from src/VisualStudio/ProjectPackage/ShadowDesigner/CompanionProjectWriter.cs
rename to src/VisualStudio/ProjectSystemCPS/ShadowDesigner/CompanionProjectWriter.cs
index 29c25cc153..4c3c5dacbc 100644
--- a/src/VisualStudio/ProjectPackage/ShadowDesigner/CompanionProjectWriter.cs
+++ b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/CompanionProjectWriter.cs
@@ -1,18 +1,21 @@
-//
+//
// Copyright (c) XSharp B.V. All Rights Reserved.
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//
-using Microsoft.VisualStudio.Project;
+// Moved from ProjectPackage, which suppresses VSTHRD010 for the whole project: the UI thread
+// requirements of this code are handled explicitly (ThreadHelper) and were not rewritten.
+#pragma warning disable VSTHRD010
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
+using System.Text.RegularExpressions;
using System.Xml.Linq;
-namespace XSharp.Project.ShadowDesigner
+namespace XSharp.ProjectSystem.ShadowDesigner
{
///
/// Generates/refreshes a plain C# companion project (sibling to the real .xsproj's
@@ -21,6 +24,9 @@ namespace XSharp.Project.ShadowDesigner
///
internal static class CompanionProjectWriter
{
+ /// Companion project name = real project name + this suffix (also its folder name).
+ internal const string CompanionSuffix = ".ShadowDesigner";
+
public readonly struct CompanionPaths
{
public CompanionPaths(string csprojPath, string designerCsPath)
@@ -59,7 +65,7 @@ public PackageRef(string include, string version)
public static CompanionPaths ComputePaths(string realXsprojPath, string className)
{
string companionDir = GetCompanionDir(realXsprojPath);
- string csprojName = Path.GetFileNameWithoutExtension(realXsprojPath) + ".ShadowDesigner";
+ string csprojName = Path.GetFileNameWithoutExtension(realXsprojPath) + CompanionSuffix;
return new CompanionPaths(
Path.Combine(companionDir, csprojName + ".csproj"),
Path.Combine(companionDir, className + ".Designer.cs"));
@@ -78,20 +84,34 @@ private static string GetCompanionDir(string realXsprojPath)
string realProjectName = Path.GetFileNameWithoutExtension(realXsprojPath);
string solutionParentDir = Path.GetDirectoryName(realProjectDir)
?? throw new InvalidOperationException($"Could not determine parent directory of {realProjectDir}.");
- return Path.Combine(solutionParentDir, realProjectName + ".ShadowDesigner");
+ return Path.Combine(solutionParentDir, realProjectName + CompanionSuffix);
}
+ ///
+ /// Folder of the companion project of .
+ ///
+ public static string ComputeCompanionDir(string realXsprojPath) => GetCompanionDir(realXsprojPath);
+
+ /// The X# project.
+ /// The active target framework of the loaded project (short form, e.g.
+ /// net8.0), or null to read it from the project file.
+ /// Filtered assembly references of the X# project.
+ /// The merged form class (.prg + .Designer.prg) as C#.
+ /// The Form1.cs-equivalent stub: the partial class with the same base types and
+ /// imports as the generated designer code (see ShadowDesignerBridge.BuildStubCSharp).
+ /// The form class.
public static CompanionPaths EnsureCompanionProject(
string realXsprojPath,
+ string evaluatedTargetFramework,
IReadOnlyList referencePaths,
string generatedDesignerCsharp,
- string namespaceName,
+ string stubCsharp,
string className)
{
string companionDir = GetCompanionDir(realXsprojPath);
Directory.CreateDirectory(companionDir);
- string targetFramework = ReadTargetFramework(realXsprojPath);
+ string targetFramework = GetTargetFramework(realXsprojPath, evaluatedTargetFramework);
// Read items directly out of the real .xsproj's XML (Include +
// Version only, no MSBuild evaluation) and emit equivalent items into the
@@ -114,26 +134,42 @@ public static CompanionPaths EnsureCompanionProject(
WriteIfChanged(paths.CsprojPath, BuildCsprojContent(targetFramework, filteredReferencePaths, packageReferences));
WriteIfChanged(paths.DesignerCsPath, generatedDesignerCsharp);
- // Only written once -- an "ordinary user file" slot, not regenerated each run, so
- // a future manual tweak here survives re-runs.
+ // The stub is where the Designer puts new members (e.g. event handler stubs, picked up by
+ // EventHandlerSync), so it is only regenerated while it is still an empty generated stub -- e.g. one
+ // that was written with an outdated base class.
string stubCsPath = ComputeFormCsPath(realXsprojPath, className);
- if (!File.Exists(stubCsPath))
+ if (!File.Exists(stubCsPath) || IsEmptyStub(File.ReadAllText(stubCsPath)))
{
- File.WriteAllText(stubCsPath, BuildStubContent(namespaceName, className));
+ WriteIfChanged(stubCsPath, stubCsharp);
}
return paths;
}
- private static string ReadTargetFramework(string xsprojPath)
+ ///
+ /// The target framework of the companion project: the evaluated (active) target framework of the loaded
+ /// project, which also covers multi-targeting and a TargetFramework set in Directory.Build.props; otherwise
+ /// the first of <TargetFramework> / <TargetFrameworks> in the project file.
+ ///
+ private static string GetTargetFramework(string xsprojPath, string evaluatedTargetFramework)
{
- var doc = XDocument.Load(xsprojPath);
- var element = doc.Descendants(XSharpProjectFileConstants.TargetFramework).FirstOrDefault();
- if (element == null)
+ string tfm = evaluatedTargetFramework;
+ if (string.IsNullOrEmpty(tfm))
{
- throw new InvalidOperationException($"No element found in {xsprojPath}.");
+ var doc = XDocument.Load(xsprojPath);
+ tfm = doc.Descendants(XSharpProjectFileConstants.TargetFramework).FirstOrDefault()?.Value?.Trim();
+ if (string.IsNullOrEmpty(tfm))
+ {
+ tfm = doc.Descendants("TargetFrameworks").FirstOrDefault()?.Value
+ ?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(t => t.Trim())
+ .FirstOrDefault(t => t.Length > 0 && t.IndexOf('$') < 0);
+ }
+ if (string.IsNullOrEmpty(tfm))
+ {
+ throw new InvalidOperationException($"Could not determine the target framework of {xsprojPath}.");
+ }
}
- string tfm = element.Value;
// The out-of-process Designer's host-process launcher requires a Windows-Desktop
// TFM (observed empirically as a "Timed out while connecting to the named pipe"
@@ -204,27 +240,32 @@ internal static List ReadPackageReferences(string xsprojPath)
{
var doc = XDocument.Load(xsprojPath);
var result = new List();
- foreach (var el in doc.Descendants(ProjectFileConstants.PackageReference))
+ foreach (var el in doc.Descendants("PackageReference"))
{
- string include = el.Attribute(ProjectFileConstants.Include)?.Value;
+ string include = el.Attribute("Include")?.Value;
if (string.IsNullOrEmpty(include)) continue;
- string version = el.Attribute(ProjectFileConstants.Version)?.Value
- ?? el.Element(ProjectFileConstants.Version)?.Value;
+ string version = el.Attribute("Version")?.Value
+ ?? el.Element("Version")?.Value;
result.Add(new PackageRef(include, version));
}
return result;
}
- private static string BuildStubContent(string namespaceName, string className)
+ // An empty generated stub: comments, usings, optional namespace and a partial class without members
+ private static readonly Regex EmptyStubRegex = new Regex(
+ @"^\s*(namespace\s+[\w.]+\s*\{)?\s*public\s+partial\s+class\s+\w+(\s*:[^{]*)?\s*\{\s*\}\s*\}?\s*$",
+ RegexOptions.Singleline);
+
+ ///
+ /// True when is an empty generated stub (from this writer), i.e. a file that can be
+ /// regenerated without losing anything.
+ ///
+ internal static bool IsEmptyStub(string content)
{
- return
-$@"namespace {namespaceName}
-{{
- public partial class {className} : System.Windows.Forms.Form
- {{
- }}
-}}
-";
+ var lines = content.Replace("\r", "").Split('\n')
+ .Select(l => l.Trim())
+ .Where(l => !l.StartsWith("//", StringComparison.Ordinal) && !(l.StartsWith("using ", StringComparison.Ordinal) && l.EndsWith(";", StringComparison.Ordinal)));
+ return EmptyStubRegex.IsMatch(string.Join("\n", lines));
}
private static string Escape(string value) => System.Security.SecurityElement.Escape(value);
diff --git a/src/VisualStudio/ProjectPackage/ShadowDesigner/CompanionSaveWatcher.cs b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/CompanionSaveWatcher.cs
similarity index 93%
rename from src/VisualStudio/ProjectPackage/ShadowDesigner/CompanionSaveWatcher.cs
rename to src/VisualStudio/ProjectSystemCPS/ShadowDesigner/CompanionSaveWatcher.cs
index e2ecb91a21..7ab099f127 100644
--- a/src/VisualStudio/ProjectPackage/ShadowDesigner/CompanionSaveWatcher.cs
+++ b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/CompanionSaveWatcher.cs
@@ -3,6 +3,9 @@
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//
+// Moved from ProjectPackage, which suppresses VSTHRD010 for the whole project: the UI thread
+// requirements of this code are handled explicitly (ThreadHelper) and were not rewritten.
+#pragma warning disable VSTHRD010
using System;
using System.Collections.Generic;
using System.IO;
@@ -14,14 +17,12 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.VisualStudio;
-using Microsoft.VisualStudio.Project;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Threading;
-using Logger = XSharp.Project.Logger;
-namespace XSharp.Project.ShadowDesigner
+namespace XSharp.ProjectSystem.ShadowDesigner
{
///
/// Automatically runs the two manual sync commands (EventHandlerSync then
@@ -65,10 +66,10 @@ private CompanionSaveWatcher(IVsRunningDocumentTable rdt, DTE2 dte)
///
/// Registers a companion's paths for auto-sync-on-save, lazily advising the RDT the
/// first time this is called in the session. Safe to call every time
- /// ShadowDesignerBridge.TryOpen succeeds -- idempotent (overwrites any existing
+ /// ShadowDesignerBridge.TryOpenAsync prepares the companion -- idempotent (overwrites any existing
/// entry for the same paths).
///
- public static void Watch(ProjectNode projectMgr, ShadowDesignerBridge.CompanionLocation location)
+ public static void Watch(IServiceProvider services, ShadowDesignerBridge.CompanionLocation location)
{
ThreadHelper.ThrowIfNotOnUIThread();
if (_instance == null)
@@ -77,8 +78,8 @@ public static void Watch(ProjectNode projectMgr, ShadowDesignerBridge.CompanionL
{
if (_instance == null)
{
- var rdt = projectMgr.GetService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable;
- var dte = projectMgr.GetService(typeof(SDTE)) as DTE2;
+ var rdt = services.GetService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable;
+ var dte = services.GetService(typeof(SDTE)) as DTE2;
if (rdt == null || dte == null)
{
return;
@@ -218,12 +219,14 @@ public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowF
// above is unaffected.
//
// Deliberately fire-and-forget (this call must return now so the frame can
- // finish showing) -- uses the package's own JoinableTaskFactory rather than the
- // static ThreadHelper one so VS can still track/join the pending operation.
- XSharpProjectPackage.XInstance.JoinableTaskFactory.RunAsync(async () =>
+ // finish showing) -- ThreadHelper.JoinableTaskFactory lets VS track/join the pending operation.
+ // VSSDK007 wants a package JoinableTaskFactory; this static, package independent code uses
+ // ThreadHelper and hands the task to FileAndForget (fault reporting) instead.
+#pragma warning disable VSSDK007
+ ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
{
await Task.Yield();
- await XSharpProjectPackage.XInstance.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
try
{
string handlerName = TryGetCaretMethodName(pFrame, path);
@@ -237,7 +240,8 @@ public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowF
{
_syncing = false;
}
- }).Task.FileAndForget("XSharp/ShadowDesigner/ExistingHandlerRedirect");
+ }).FileAndForget("XSharp/ShadowDesigner/ExistingHandlerRedirect");
+#pragma warning restore VSSDK007
return VSConstants.S_OK;
}
diff --git a/src/VisualStudio/ProjectPackage/ShadowDesigner/DesignerChangesSync.cs b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/DesignerChangesSync.cs
similarity index 95%
rename from src/VisualStudio/ProjectPackage/ShadowDesigner/DesignerChangesSync.cs
rename to src/VisualStudio/ProjectSystemCPS/ShadowDesigner/DesignerChangesSync.cs
index 64713de4b8..e7f9dd444f 100644
--- a/src/VisualStudio/ProjectPackage/ShadowDesigner/DesignerChangesSync.cs
+++ b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/DesignerChangesSync.cs
@@ -3,6 +3,9 @@
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//
+// Moved from ProjectPackage, which suppresses VSTHRD010 for the whole project: the UI thread
+// requirements of this code are handled explicitly (ThreadHelper) and were not rewritten.
+#pragma warning disable VSTHRD010
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
@@ -13,7 +16,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using XSharp.CodeDom;
-namespace XSharp.Project.ShadowDesigner
+namespace XSharp.ProjectSystem.ShadowDesigner
{
///
/// "Designer -> Code, beyond handler creation" (general property edits, control
@@ -31,7 +34,7 @@ namespace XSharp.Project.ShadowDesigner
/// wiring (+=) lines into Form1.Designer.prg, via targeted regex insertion rather than
/// a full regenerate. Run event sync before this one when both apply in the same pass.
///
- internal static class DesignerChangesSync
+ public static class DesignerChangesSync
{
public sealed class SyncResult
{
diff --git a/src/VisualStudio/ProjectPackage/ShadowDesigner/EventHandlerSync.cs b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/EventHandlerSync.cs
similarity index 97%
rename from src/VisualStudio/ProjectPackage/ShadowDesigner/EventHandlerSync.cs
rename to src/VisualStudio/ProjectSystemCPS/ShadowDesigner/EventHandlerSync.cs
index 1d1b73d098..60c257125b 100644
--- a/src/VisualStudio/ProjectPackage/ShadowDesigner/EventHandlerSync.cs
+++ b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/EventHandlerSync.cs
@@ -3,6 +3,9 @@
// Licensed under the Apache License, Version 2.0.
// See License.txt in the project root for license information.
//
+// Moved from ProjectPackage, which suppresses VSTHRD010 for the whole project: the UI thread
+// requirements of this code are handled explicitly (ThreadHelper) and were not rewritten.
+#pragma warning disable VSTHRD010
using System;
using System.Collections.Generic;
using System.IO;
@@ -12,7 +15,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-namespace XSharp.Project.ShadowDesigner
+namespace XSharp.ProjectSystem.ShadowDesigner
{
///
/// Detects event-handler stubs and wiring statements the out-of-process Designer added
@@ -22,7 +25,7 @@ namespace XSharp.Project.ShadowDesigner
/// since Microsoft.CSharp.CSharpCodeProvider.Parse() doesn't implement C# parsing at all
/// (CodeDom only ever supported generation, not parsing, for this provider).
///
- internal static class EventHandlerSync
+ public static class EventHandlerSync
{
public sealed class SyncResult
{
diff --git a/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/Logger.cs b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/Logger.cs
new file mode 100644
index 0000000000..5a9c3f9384
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/Logger.cs
@@ -0,0 +1,20 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+using System;
+using XSharp.Settings;
+
+namespace XSharp.ProjectSystem.ShadowDesigner
+{
+ ///
+ /// Writes to the X# log (XSettings.Logger), like XSharp.Project.Logger in the X# project package.
+ ///
+ internal static class Logger
+ {
+ internal static void Exception(Exception e, string msg) => XSettings.Logger.Exception(e, msg);
+ internal static void Information(string msg) => XSettings.Logger.Information(msg);
+ internal static void Error(string msg) => XSettings.Logger.Error(msg);
+ }
+}
diff --git a/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/NewFormDesignerRedirect.cs b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/NewFormDesignerRedirect.cs
new file mode 100644
index 0000000000..515c62d1f4
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/NewFormDesignerRedirect.cs
@@ -0,0 +1,176 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using Microsoft.VisualStudio.Threading;
+using XSharpModel;
+
+namespace XSharp.ProjectSystem.ShadowDesigner
+{
+ ///
+ /// Opens a form or user control that was just added with "Add New Item" in the shadow designer instead of the
+ /// code editor.
+ ///
+ ///
+ /// The item template wizard opens the new .prg itself (primary view), so it does not go through the shadow
+ /// designer commands (double click, View Designer); VS picks the default editor for .prg, the X# code editor.
+ /// MPFproj redirected this in XSharpFileNode, which is gone together with the MPFproj SDK path.
+ /// CPS' own mechanism for "forms open in the designer" (IProjectSpecificEditorProvider with IsDefaultEditor) is
+ /// not usable: it routes every open of the file to the project specific editor factory, also the debugger and
+ /// navigation (LOGVIEWID_Debugging/TextView), which the shadow designer cannot serve.
+ /// Instead this watches the first window of documents of CPS X# projects: when it belongs to a form .prg (a
+ /// .Designer.prg exists) whose file was created just now (by the template), the shadow designer is opened and,
+ /// once that succeeded, the code window is closed. Every file is redirected at most once per session, so a later
+ /// "View Code" is not affected. "Add Existing Item" opens no window, so it is not affected either.
+ ///
+ internal sealed class NewFormDesignerRedirect : IVsRunningDocTableEvents
+ {
+ // A file created longer ago is not a new item from a template
+ private static readonly TimeSpan NewFileAge = TimeSpan.FromSeconds(30);
+ // How long to wait for the code model to know the new file (evaluation of the project)
+ private static readonly TimeSpan CodeModelTimeout = TimeSpan.FromSeconds(10);
+
+ private static NewFormDesignerRedirect _instance;
+
+ private readonly IVsRunningDocumentTable _rdt;
+ private readonly JoinableTaskFactory _joinableTaskFactory;
+ private readonly HashSet _handled = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ private NewFormDesignerRedirect(IVsRunningDocumentTable rdt, JoinableTaskFactory joinableTaskFactory)
+ {
+ _rdt = rdt;
+ _joinableTaskFactory = joinableTaskFactory;
+ }
+
+ ///
+ /// Starts watching the document windows, once per session. Called when the first X# CPS project loads, with
+ /// the JoinableTaskFactory of the CPS threading service (runs the switch to the designer).
+ ///
+ public static void EnsureAdvised(JoinableTaskFactory joinableTaskFactory)
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ if (_instance != null)
+ {
+ return;
+ }
+ if (!(ServiceProvider.GlobalProvider.GetService(typeof(SVsRunningDocumentTable)) is IVsRunningDocumentTable rdt))
+ {
+ return;
+ }
+ var instance = new NewFormDesignerRedirect(rdt, joinableTaskFactory);
+ if (ErrorHandler.Succeeded(rdt.AdviseRunningDocTableEvents(instance, out uint _)))
+ {
+ // Process lifetime: the table outlives every solution, there is nothing to unadvise before shutdown
+ _instance = instance;
+ }
+ }
+
+ public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame)
+ {
+ ThreadHelper.ThrowIfNotOnUIThread();
+ if (fFirstShow == 0 || pFrame == null)
+ {
+ return VSConstants.S_OK;
+ }
+ try
+ {
+ if (ErrorHandler.Failed(_rdt.GetDocumentInfo(docCookie, out uint _, out uint _, out uint _, out string moniker,
+ out IVsHierarchy hierarchy, out uint _, out IntPtr docData)))
+ {
+ return VSConstants.S_OK;
+ }
+ if (docData != IntPtr.Zero)
+ {
+ Marshal.Release(docData);
+ }
+ string mainPrg = GetMainPrg(moniker);
+ if (mainPrg == null || hierarchy == null || !hierarchy.IsCapabilityMatch(XSharpCapabilities.XSharpCps) ||
+ !IsNewFile(mainPrg) || !_handled.Add(mainPrg))
+ {
+ return VSConstants.S_OK;
+ }
+ Logger.Information($"NewFormDesignerRedirect: new form {mainPrg} opened in the code editor, switching to the designer");
+ _joinableTaskFactory.RunAsync(() => SwitchToDesignerAsync(mainPrg, pFrame))
+ .FileAndForget("XSharp/ProjectSystemCPS/NewFormDesignerRedirect");
+ }
+ catch (Exception ex)
+ {
+ Logger.Exception(ex, "NewFormDesignerRedirect.OnBeforeDocumentWindowShow");
+ }
+ return VSConstants.S_OK;
+ }
+
+ ///
+ /// Form1.prg or Form1.Designer.prg -> Form1.prg, for files of an X# form (the template can open either).
+ ///
+ private static string GetMainPrg(string moniker)
+ {
+ if (string.IsNullOrEmpty(moniker) || !moniker.EndsWith(".prg", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ const string designerSuffix = ".designer.prg";
+ if (moniker.EndsWith(designerSuffix, StringComparison.OrdinalIgnoreCase))
+ {
+ return moniker.Substring(0, moniker.Length - designerSuffix.Length) + ".prg";
+ }
+ return moniker;
+ }
+
+ private static bool IsNewFile(string path)
+ {
+ try
+ {
+ return File.Exists(path) && DateTime.UtcNow - File.GetCreationTimeUtc(path) < NewFileAge;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ private static async Task SwitchToDesignerAsync(string mainPrg, IVsWindowFrame codeFrame)
+ {
+ // Let the Add New Item operation finish; the .Designer.prg is written by the same template and the project
+ // (and with it the code model) only knows the new files after its next evaluation.
+ var deadline = DateTime.UtcNow + CodeModelTimeout;
+ while (!(ShadowDesignerBridge.HasDesignerFile(mainPrg) && XSolution.FindFile(mainPrg)?.Project != null))
+ {
+ if (DateTime.UtcNow >= deadline)
+ {
+ Logger.Information($"NewFormDesignerRedirect: {mainPrg} is not known to the X# code model, keeping the code editor");
+ return;
+ }
+ await Task.Delay(200);
+ }
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ if (!await XSharpShadowDesignerCommands.TryOpenAsync(mainPrg))
+ {
+ // The designer could not be opened: the code editor stays, as without this redirect
+ return;
+ }
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ // A document from a template is not dirty; PromptSave only asks if the user already typed something
+ int hr = codeFrame.CloseFrame((uint)__FRAMECLOSE.FRAMECLOSE_PromptSave);
+ Logger.Information($"NewFormDesignerRedirect: closed the code editor of {mainPrg}, hr=0x{hr:X8}");
+ }
+
+ #region IVsRunningDocTableEvents members that are not used
+ public int OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => VSConstants.S_OK;
+ public int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => VSConstants.S_OK;
+ public int OnAfterSave(uint docCookie) => VSConstants.S_OK;
+ public int OnAfterAttributeChange(uint docCookie, uint grfAttribs) => VSConstants.S_OK;
+ public int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame) => VSConstants.S_OK;
+ #endregion
+ }
+}
diff --git a/src/VisualStudio/ProjectPackage/ShadowDesigner/RoslynToCodeDom.cs b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/RoslynToCodeDom.cs
similarity index 98%
rename from src/VisualStudio/ProjectPackage/ShadowDesigner/RoslynToCodeDom.cs
rename to src/VisualStudio/ProjectSystemCPS/ShadowDesigner/RoslynToCodeDom.cs
index ad96f78aeb..b10e95aad6 100644
--- a/src/VisualStudio/ProjectPackage/ShadowDesigner/RoslynToCodeDom.cs
+++ b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/RoslynToCodeDom.cs
@@ -1,3 +1,6 @@
+// Moved from ProjectPackage, which suppresses VSTHRD010 for the whole project: the UI thread
+// requirements of this code are handled explicitly (ThreadHelper) and were not rewritten.
+#pragma warning disable VSTHRD010
using System;
using System.CodeDom;
using System.Collections.Generic;
@@ -6,7 +9,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-namespace XSharp.Project.ShadowDesigner
+namespace XSharp.ProjectSystem.ShadowDesigner
{
///
/// Translates the statement shapes the out-of-process WinForms Designer emits into the
diff --git a/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/ShadowDesignerBridge.cs b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/ShadowDesignerBridge.cs
new file mode 100644
index 0000000000..6c1403f033
--- /dev/null
+++ b/src/VisualStudio/ProjectSystemCPS/ShadowDesigner/ShadowDesignerBridge.cs
@@ -0,0 +1,728 @@
+//
+// Copyright (c) XSharp B.V. All Rights Reserved.
+// Licensed under the Apache License, Version 2.0.
+// See License.txt in the project root for license information.
+//
+// Moved from ProjectPackage, which suppresses VSTHRD010 for the whole project: the UI thread
+// requirements of this code are handled explicitly (ThreadHelper) and were not rewritten.
+#pragma warning disable VSTHRD010
+using System;
+using System.CodeDom;
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Versioning;
+using System.Threading.Tasks;
+using EnvDTE80;
+using Microsoft.CSharp;
+using Microsoft.VisualStudio;
+using Microsoft.VisualStudio.OperationProgress;
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
+using XSharp.CodeDom;
+using XSharp.Settings;
+using XSharpModel;
+
+namespace XSharp.ProjectSystem.ShadowDesigner
+{
+ ///
+ /// Entry point for the "View Designer on a SDK-style .prg" bridge. VS's out-of-process
+ /// WinForms Designer (used for .NET Core/SDK-style projects) has no extensibility point
+ /// for third-party languages, so a .prg can never be opened in it directly. This bridges
+ /// the gap: parse the real .prg/.Designer.prg with the real X# parser, merge into one
+ /// CodeCompileUnit, generate C# via the stock BCL CSharpCodeProvider, write/refresh a
+ /// real, hidden, auto-generated companion C# project next to the real one, and open ITS
+ /// Designer view (whose owning IVsHierarchy is a real C# project, so the out-of-process
+ /// Designer's project-type gate passes).
+ ///
+ /// XSharpCodeParser/XSharpCodeDomHelper/XProject are called directly -- no reflection is
+ /// needed to reach them. The bridge only needs the path of the .prg and the X# code model
+ /// (XProject) of its project. Entry points: the CPS commands XSharpShadowDesignerDefaultActionCommand and
+ /// XSharpShadowDesignerViewFormCommand (MPFproj no longer loads SDK-style projects).
+ ///
+ /// Also exposes