diff --git a/Assets/Plugins/Crazy Minnow Studio/Examples/Scenes/SALSA-basic3D-boxhead.unity b/Assets/Plugins/Crazy Minnow Studio/Examples/Scenes/SALSA-basic3D-boxhead.unity index b341152f1f..e77442f267 100644 --- a/Assets/Plugins/Crazy Minnow Studio/Examples/Scenes/SALSA-basic3D-boxhead.unity +++ b/Assets/Plugins/Crazy Minnow Studio/Examples/Scenes/SALSA-basic3D-boxhead.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d5b5f035c5272eee77cdafb12c97f11f87ae7a9a4d5e0e0d74cc7b962daefa0 -size 121348 +oid sha256:c2a972840e32219bdb153028f1e9627ada544af6f15b32dcff28d55fa7958732 +size 83085 diff --git a/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/Hack-Regular SDF.asset b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/Hack-Regular SDF.asset new file mode 100644 index 0000000000..7ff1d98d49 --- /dev/null +++ b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/Hack-Regular SDF.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00e78ce012967093d632f88bb9f79cabfff54697e353aa62d11e8819d2b31abf +size 581598 diff --git a/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/Hack-Regular SDF.asset.meta b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/Hack-Regular SDF.asset.meta new file mode 100644 index 0000000000..2443b06dbc --- /dev/null +++ b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/Hack-Regular SDF.asset.meta @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:824af23fc6e295e42273bcf0b2b64348ff7fb4c5a8d94c67c1b70ffe43c04c51 +size 189 diff --git a/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset index 298319b5a9..f9da0fe095 100644 --- a/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset +++ b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db21ab969715d43941790e5836777631684837c69a3eea1a60482d410902e142 -size 540580 +oid sha256:7d402aa48c455f81c272200122948e01879024adff0987d3c4de3f7d3a876960 +size 572009 diff --git a/Assets/Resources/Materials/PortalSpriteMaterial.mat b/Assets/Resources/Materials/PortalSpriteMaterial.mat index e473f7476d..df329849d7 100644 --- a/Assets/Resources/Materials/PortalSpriteMaterial.mat +++ b/Assets/Resources/Materials/PortalSpriteMaterial.mat @@ -2,20 +2,24 @@ %TAG !u! tag:unity3d.com,2011: --- !u!21 &2100000 Material: - serializedVersion: 6 + serializedVersion: 8 m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: PortalSpriteMaterial m_Shader: {fileID: 4800000, guid: f6846526bc14481aad2e5b443c928c65, type: 3} - m_ShaderKeywords: + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] m_LightmapFlags: 4 m_EnableInstancingVariants: 0 m_DoubleSidedGI: 0 m_CustomRenderQueue: 4000 stringTagMap: {} disabledShaderPasses: [] + m_LockedProperties: m_SavedProperties: serializedVersion: 3 m_TexEnvs: @@ -55,6 +59,7 @@ Material: m_Texture: {fileID: 0} m_Scale: {x: 1, y: 1} m_Offset: {x: 0, y: 0} + m_Ints: [] m_Floats: - PixelSnap: 0 - _BumpScale: 1 @@ -76,6 +81,6 @@ Material: m_Colors: - _Color: {r: 1, g: 1, b: 1, a: 1} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - - _PortalMax: {r: -0.24699998, g: 0.511, b: 0, a: 0} - - _PortalMin: {r: -1.247, g: -0.489, b: 0, a: 0} + - _PortalMax: {r: -0.94397986, g: -1.8898091, b: 0, a: 0} + - _PortalMin: {r: -2.41602, g: -5.110191, b: 0, a: 0} m_BuildTextureStacks: [] diff --git a/Assets/Resources/Prefabs/UI/Button.prefab b/Assets/Resources/Prefabs/UI/Button.prefab index 8f56791638..8e8c9d65ce 100644 --- a/Assets/Resources/Prefabs/UI/Button.prefab +++ b/Assets/Resources/Prefabs/UI/Button.prefab @@ -28,9 +28,9 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 6586710328604211363} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} @@ -65,10 +65,11 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: BUTTON + m_text: Button m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: 84dd14695854bbc43a5faa24fcf93d0d, type: 2} - m_sharedMaterial: {fileID: 21261991626553910, guid: 84dd14695854bbc43a5faa24fcf93d0d, type: 2} + m_sharedMaterial: {fileID: 21261991626553910, guid: 84dd14695854bbc43a5faa24fcf93d0d, + type: 2} m_fontSharedMaterials: [] m_fontMaterial: {fileID: 0} m_fontMaterials: [] @@ -144,7 +145,7 @@ GameObject: m_Component: - component: {fileID: 8426220730825732241} - component: {fileID: 6175946636808426846} - - component: {fileID: 1531332202938432071} + - component: {fileID: 8382553333224995676} m_Layer: 5 m_Name: Icon m_TagString: Untagged @@ -162,9 +163,9 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 6586710328604211363} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0.5} m_AnchorMax: {x: 0, y: 0.5} @@ -179,7 +180,7 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 4744790295661001541} m_CullTransparentMesh: 0 ---- !u!114 &1531332202938432071 +--- !u!114 &8382553333224995676 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -188,27 +189,87 @@ MonoBehaviour: m_GameObject: {fileID: 4744790295661001541} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} m_Name: m_EditorClassIdentifier: m_Material: {fileID: 0} - m_Color: {r: 0.1764706, g: 0.25490198, b: 0.33333334, a: 1} + m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_Sprite: {fileID: 21300000, guid: a8cc5f0db692cb24db144d85c01b6838, type: 3} - m_Type: 0 - m_PreserveAspect: 1 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 + m_text: '?' + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 4ebb98a3c87fa521a888029274c92b79, type: 2} + m_sharedMaterial: {fileID: -8620075009897487826, guid: 4ebb98a3c87fa521a888029274c92b79, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4278190080 + m_fontColor: {r: 0, g: 0, b: 0, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 30 + m_fontSizeBase: 30 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} --- !u!1 &4918872672529199024 GameObject: m_ObjectHideFlags: 0 @@ -238,9 +299,9 @@ RectTransform: m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 6586710328604211363} - m_RootOrder: 2 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} @@ -308,10 +369,10 @@ GameObject: m_Component: - component: {fileID: 6586710328604211363} - component: {fileID: 2917540488982011779} - - component: {fileID: 8210310399375922163} - component: {fileID: 2539900339822752133} - component: {fileID: 436568625700612899} - component: {fileID: 6827147320954825169} + - component: {fileID: 5444878965105383002} m_Layer: 5 m_Name: Button m_TagString: Untagged @@ -329,12 +390,12 @@ RectTransform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 8426220730825732241} - {fileID: 7086379228738207378} - {fileID: 6595404481868969509} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5} @@ -349,46 +410,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 7702164899860693017} m_CullTransparentMesh: 0 ---- !u!114 &8210310399375922163 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 7702164899860693017} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: ddd611441fc6f1f4c9f714fcd4dcbeb3, type: 3} - m_Name: - m_EditorClassIdentifier: - buttonIcon: {fileID: 21300000, guid: a8cc5f0db692cb24db144d85c01b6838, type: 3} - buttonText: BUTTON - clickEvent: - m_PersistentCalls: - m_Calls: [] - hoverEvent: - m_PersistentCalls: - m_Calls: [] - hoverSound: {fileID: 0} - clickSound: {fileID: 0} - buttonVar: {fileID: 0} - normalImage: {fileID: 1531332202938432071} - normalText: {fileID: 7271999502370158117} - soundSource: {fileID: 0} - rippleParent: {fileID: 4918872672529199024} - useCustomContent: 0 - enableButtonSounds: 0 - useHoverSound: 1 - useClickSound: 1 - useRipple: 1 - rippleUpdateMode: 1 - rippleShape: {fileID: 21300000, guid: d25e2ce15dd1c67438e4b70f404fb197, type: 3} - speed: 2.4 - maxSize: 6 - startColor: {r: 0, g: 0, b: 0, a: 0.39215687} - transitionColor: {r: 0, g: 0, b: 0, a: 0} - renderOnTop: 0 - centered: 0 --- !u!114 &2539900339822752133 MonoBehaviour: m_ObjectHideFlags: 0 @@ -475,3 +496,41 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 52dd8aaa3b5d4058ac1b1e9242d35f57, type: 3} m_Name: m_EditorClassIdentifier: +--- !u!114 &5444878965105383002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7702164899860693017} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1a12ddbc47b17cd478cb447d1113a22b, type: 3} + m_Name: + m_EditorClassIdentifier: + buttonText: Button + clickEvent: + m_PersistentCalls: + m_Calls: [] + hoverEvent: + m_PersistentCalls: + m_Calls: [] + hoverSound: {fileID: 0} + clickSound: {fileID: 0} + buttonVar: {fileID: 0} + normalText: {fileID: 7271999502370158117} + soundSource: {fileID: 0} + rippleParent: {fileID: 4918872672529199024} + useCustomContent: 0 + enableButtonSounds: 0 + useHoverSound: 1 + useClickSound: 1 + useRipple: 1 + rippleUpdateMode: 1 + rippleShape: {fileID: 21300000, guid: d25e2ce15dd1c67438e4b70f404fb197, type: 3} + speed: 2.4 + maxSize: 6 + startColor: {r: 1, g: 1, b: 1, a: 1} + transitionColor: {r: 1, g: 1, b: 1, a: 0} + renderOnTop: 0 + centered: 0 diff --git a/Assets/Resources/Prefabs/UI/CodeWindowContent.prefab b/Assets/Resources/Prefabs/UI/CodeWindowContent.prefab index faffcf96bd..d64fb9ec78 100644 --- a/Assets/Resources/Prefabs/UI/CodeWindowContent.prefab +++ b/Assets/Resources/Prefabs/UI/CodeWindowContent.prefab @@ -684,7 +684,7 @@ MonoBehaviour: m_lineSpacingMax: 0 m_paragraphSpacing: 0 m_charWidthMaxAdj: 0 - m_enableWordWrapping: 1 + m_enableWordWrapping: 0 m_wordWrappingRatios: 0.413 m_overflowMode: 0 m_linkedTextComponent: {fileID: 0} @@ -722,7 +722,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} m_Name: m_EditorClassIdentifier: - m_HorizontalFit: 0 + m_HorizontalFit: 2 m_VerticalFit: 2 --- !u!1 &8823517661321162182 GameObject: diff --git a/Assets/Resources/Prefabs/UI/Menu.prefab b/Assets/Resources/Prefabs/UI/Menu.prefab index 14f9025144..e11a1fb41a 100644 --- a/Assets/Resources/Prefabs/UI/Menu.prefab +++ b/Assets/Resources/Prefabs/UI/Menu.prefab @@ -55,8 +55,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -130,8 +130,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -206,8 +206,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Padding: m_Left: 0 m_Right: 0 @@ -282,8 +282,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: ef1b9ec3e5bcdc64d87d311ffc627a77, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: buttonIcon: {fileID: 21300000, guid: 5d08ed2465c5c104c9c915959d69b527, type: 3} buttonText: CLOSE clickEvent: @@ -326,8 +326,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Navigation: m_Mode: 0 m_WrapAround: 0 @@ -361,7 +361,7 @@ MonoBehaviour: m_PersistentCalls: m_Calls: - m_Target: {fileID: 2057475846036032719} - m_TargetAssemblyTypeName: + m_TargetAssemblyTypeName: m_MethodName: CloseWindow m_Mode: 1 m_Arguments: @@ -369,7 +369,7 @@ MonoBehaviour: m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine m_IntArgument: 0 m_FloatArgument: 0 - m_StringArgument: + m_StringArgument: m_BoolArgument: 0 m_CallState: 2 --- !u!95 &1753449128007808263 @@ -388,7 +388,7 @@ Animator: m_ApplyRootMotion: 0 m_LinearVelocityBlending: 0 m_StabilizeFeet: 0 - m_WarningMessage: + m_WarningMessage: m_HasTransformHierarchy: 1 m_AllowConstantClipSamplingOptimization: 1 m_KeepAnimatorStateOnDisable: 0 @@ -403,8 +403,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 0} m_RaycastTarget: 1 @@ -478,8 +478,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -615,8 +615,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Padding: m_Left: 0 m_Right: 0 @@ -689,8 +689,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -719,8 +719,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_ShowMaskGraphic: 0 --- !u!225 &8644951742806170891 CanvasGroup: @@ -789,8 +789,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -927,8 +927,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1017,8 +1017,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_HorizontalFit: 2 m_VerticalFit: 0 --- !u!1 &2574983841249248955 @@ -1076,8 +1076,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1155,8 +1155,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 8ff5b50d8ff89864090b86d1fee33b66, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: windowIcon: {fileID: 1059299024181343886} windowTitle: {fileID: 196948630522393265} windowDescription: {fileID: 5106170833735170336} @@ -1205,7 +1205,7 @@ Animator: m_ApplyRootMotion: 0 m_LinearVelocityBlending: 0 m_StabilizeFeet: 0 - m_WarningMessage: + m_WarningMessage: m_HasTransformHierarchy: 1 m_AllowConstantClipSamplingOptimization: 1 m_KeepAnimatorStateOnDisable: 0 @@ -1265,8 +1265,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1340,8 +1340,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.37254903, g: 0.40784317, b: 0.45098042, a: 1} m_RaycastTarget: 1 @@ -1534,8 +1534,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Padding: m_Left: 0 m_Right: 0 @@ -1608,8 +1608,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 0.078431375} m_RaycastTarget: 1 @@ -1650,8 +1650,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_ShowMaskGraphic: 0 --- !u!1 &7199230629601579101 GameObject: @@ -1712,8 +1712,8 @@ MonoBehaviour: m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1787,8 +1787,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -1982,8 +1982,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} m_RaycastTarget: 1 @@ -2057,8 +2057,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: + m_Name: + m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0.078431375, g: 0.09803922, b: 0.11764706, a: 0.9607843} m_RaycastTarget: 1 diff --git a/Assets/Resources/Prefabs/UI/Tooltip.prefab b/Assets/Resources/Prefabs/UI/Tooltip.prefab index 8fd93f81bf..441dfc6229 100644 --- a/Assets/Resources/Prefabs/UI/Tooltip.prefab +++ b/Assets/Resources/Prefabs/UI/Tooltip.prefab @@ -12,6 +12,7 @@ GameObject: - component: {fileID: 8314083953827340342} - component: {fileID: 8435845624526155984} - component: {fileID: 8831212371131215211} + - component: {fileID: 3598303689140778297} m_Layer: 5 m_Name: Description m_TagString: Untagged @@ -35,7 +36,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 20, y: 0} + m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 0.5} --- !u!222 &8314083953827340342 @@ -60,14 +61,14 @@ MonoBehaviour: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 + m_RaycastTarget: 0 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \nsed do eiusmod - tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam." + m_text: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam. m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: 4bd810f1cbcb0f446a8f5a31453e243f, type: 2} m_sharedMaterial: {fileID: 21539420542967178, guid: 4bd810f1cbcb0f446a8f5a31453e243f, @@ -144,13 +145,111 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 69885420244690972} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} m_Name: m_EditorClassIdentifier: m_HorizontalFit: 2 m_VerticalFit: 2 +--- !u!114 &3598303689140778297 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 69885420244690972} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 0 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 +--- !u!1 &1228948427612224358 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 43498258927274717} + - component: {fileID: 8105362237625770792} + - component: {fileID: 8996534731790833375} + m_Layer: 5 + m_Name: Anchor + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &43498258927274717 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1228948427612224358} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 5516898516605623285} + m_Father: {fileID: 8952043591868529639} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 1000, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &8105362237625770792 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1228948427612224358} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalFit: 0 + m_VerticalFit: 2 +--- !u!114 &8996534731790833375 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1228948427612224358} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 0 + m_Spacing: 0 + m_ChildForceExpandWidth: 0 + m_ChildForceExpandHeight: 0 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 --- !u!1 &1287535558948415425 GameObject: m_ObjectHideFlags: 0 @@ -280,7 +379,7 @@ RectTransform: m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: - - {fileID: 5516898516605623285} + - {fileID: 43498258927274717} m_Father: {fileID: 3882864775156412529} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} @@ -324,7 +423,7 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1606745063685579807} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} m_Name: @@ -338,7 +437,7 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1606745063685579807} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} m_Name: @@ -352,7 +451,7 @@ MonoBehaviour: m_Spacing: 0 m_ChildForceExpandWidth: 1 m_ChildForceExpandHeight: 1 - m_ChildControlWidth: 1 + m_ChildControlWidth: 0 m_ChildControlHeight: 1 m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 @@ -401,7 +500,7 @@ RectTransform: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 3918185506530856587} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 @@ -409,11 +508,11 @@ RectTransform: - {fileID: 1341856025113921966} - {fileID: 8005402813758547494} - {fileID: 1028205101935243798} - m_Father: {fileID: 8952043591868529639} + m_Father: {fileID: 43498258927274717} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 370.61, y: 0} + m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} --- !u!222 &3456573861590457279 @@ -443,7 +542,7 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 3918185506530856587} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} m_Name: @@ -469,10 +568,10 @@ MonoBehaviour: m_Bottom: 15 m_ChildAlignment: 0 m_Spacing: 0 - m_ChildForceExpandWidth: 1 + m_ChildForceExpandWidth: 0 m_ChildForceExpandHeight: 0 m_ChildControlWidth: 1 - m_ChildControlHeight: 0 + m_ChildControlHeight: 1 m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 m_ReverseArrangement: 0 @@ -483,17 +582,17 @@ MonoBehaviour: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 3918185506530856587} - m_Enabled: 1 + m_Enabled: 0 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} m_Name: m_EditorClassIdentifier: m_IgnoreLayout: 0 - m_MinWidth: -1 + m_MinWidth: 0 m_MinHeight: 25 m_PreferredWidth: -1 m_PreferredHeight: -1 - m_FlexibleWidth: 100 + m_FlexibleWidth: 0 m_FlexibleHeight: -1 m_LayoutPriority: 1 --- !u!1 &5847143400247052628 diff --git a/Assets/Resources/TMP Settings.asset b/Assets/Resources/TMP Settings.asset new file mode 100644 index 0000000000..fe71797291 --- /dev/null +++ b/Assets/Resources/TMP Settings.asset @@ -0,0 +1,47 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2705215ac5b84b70bacc50632be6e391, type: 3} + m_Name: TMP Settings + m_EditorClassIdentifier: + m_enableWordWrapping: 1 + m_enableKerning: 1 + m_enableExtraPadding: 0 + m_enableTintAllSprites: 0 + m_enableParseEscapeCharacters: 1 + m_EnableRaycastTarget: 1 + m_GetFontFeaturesAtRuntime: 1 + m_missingGlyphCharacter: 0 + m_warningsDisabled: 1 + m_defaultFontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_defaultFontAssetPath: Fonts/ + m_defaultFontSize: 36 + m_defaultAutoSizeMinRatio: 0.5 + m_defaultAutoSizeMaxRatio: 2 + m_defaultTextMeshProTextContainerSize: {x: 20, y: 5} + m_defaultTextMeshProUITextContainerSize: {x: 200, y: 50} + m_autoSizeTextContainer: 0 + m_IsTextObjectScaleStatic: 0 + m_fallbackFontAssets: [] + m_matchMaterialPreset: 1 + m_defaultSpriteAsset: {fileID: 11400000, guid: c41005c129ba4d66911b75229fd70b45, + type: 2} + m_defaultSpriteAssetPath: Sprite Assets/ + m_enableEmojiSupport: 1 + m_MissingCharacterSpriteUnicode: 0 + m_defaultColorGradientPresetsPath: Color Gradient Presets/ + m_defaultStyleSheet: {fileID: 11400000, guid: f952c082cb03451daed3ee968ac6c63e, + type: 2} + m_StyleSheetsResourcePath: + m_leadingCharacters: {fileID: 4900000, guid: d82c1b31c7e74239bff1220585707d2b, type: 3} + m_followingCharacters: {fileID: 4900000, guid: fade42e8bc714b018fac513c043d323b, + type: 3} + m_UseModernHangulLineBreakingRules: 0 diff --git a/Assets/Resources/TMP Settings.asset.meta b/Assets/Resources/TMP Settings.asset.meta new file mode 100644 index 0000000000..57acada41e --- /dev/null +++ b/Assets/Resources/TMP Settings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d2da60f075e4f6f5c97361386127d06b +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/Controls/Actions/AbstractActionStateType.cs b/Assets/SEE/Controls/Actions/AbstractActionStateType.cs index 8260f35ee6..fd3c2cd50d 100644 --- a/Assets/SEE/Controls/Actions/AbstractActionStateType.cs +++ b/Assets/SEE/Controls/Actions/AbstractActionStateType.cs @@ -25,11 +25,11 @@ public abstract class AbstractActionStateType public Color Color { get; } /// - /// Path to the material of the icon for this action. + /// The FontAwesome codepoint of the icon for this action. See for more information. /// The icon itself should be a visual representation of the action. /// Will be used in the . /// - public string IconPath { get; } + public char Icon { get; } /// /// The parent of this action state type, i.e., the @@ -47,16 +47,17 @@ public abstract class AbstractActionStateType /// The Name of this ActionStateType. Must be unique. /// Description for this ActionStateType. /// Color for this ActionStateType. - /// Path to the material of the icon for this ActionStateType. + /// The icon which shall be displayed alongside this entry, + /// given as a FontAwesome codepoint. /// The group this action state type belongs to; may be null. /// If true, this action state type will be registered in . - protected AbstractActionStateType - (string name, string description, Color color, string iconPath, ActionStateTypeGroup group, bool register) + protected AbstractActionStateType(string name, string description, Color color, char icon, + ActionStateTypeGroup group, bool register) { Name = name; Description = description; Color = color; - IconPath = iconPath; + Icon = icon; group?.Add(this); if (register) { diff --git a/Assets/SEE/Controls/Actions/ActionStateType.cs b/Assets/SEE/Controls/Actions/ActionStateType.cs index 64b3ee4e19..a03f66515a 100644 --- a/Assets/SEE/Controls/Actions/ActionStateType.cs +++ b/Assets/SEE/Controls/Actions/ActionStateType.cs @@ -24,13 +24,13 @@ public class ActionStateType : AbstractActionStateType /// Description for this ActionStateType. /// The parent of this action in the nesting hierarchy in the menu. /// Color for this ActionStateType. - /// Path to the material of the icon for this ActionStateType. + /// Icon for this ActionStateType. /// Delegate to be called to create a new instance of this kind of action. /// Can be null, in which case no delegate will be called. /// If true, this action state type will be registered in . public ActionStateType(string name, string description, - Color color, string iconPath, CreateReversibleAction createReversible, ActionStateTypeGroup parent = null, bool register = true) - : base(name, description, color, iconPath, parent, register) + Color color, char icon, CreateReversibleAction createReversible, ActionStateTypeGroup parent = null, bool register = true) + : base(name, description, color, icon, parent, register) { CreateReversible = createReversible; } diff --git a/Assets/SEE/Controls/Actions/ActionStateTypeGroup.cs b/Assets/SEE/Controls/Actions/ActionStateTypeGroup.cs index d36ca9104c..238f6aaeb6 100644 --- a/Assets/SEE/Controls/Actions/ActionStateTypeGroup.cs +++ b/Assets/SEE/Controls/Actions/ActionStateTypeGroup.cs @@ -17,11 +17,11 @@ public class ActionStateTypeGroup : AbstractActionStateType /// Description for this . /// The parent of this action in the nesting hierarchy in the menu. /// Color for this . - /// Path to the material of the icon for this . + /// Icon for this , given as a FontAwesome codepoint. /// If true, this action state type will be registered in . public ActionStateTypeGroup - (string name, string description, Color color, string iconPath, ActionStateTypeGroup parent = null, bool register = true) - : base(name, description, color, iconPath, parent, register) + (string name, string description, Color color, char icon, ActionStateTypeGroup parent = null, bool register = true) + : base(name, description, color, icon, parent, register) { } diff --git a/Assets/SEE/Controls/Actions/ActionStateTypes.cs b/Assets/SEE/Controls/Actions/ActionStateTypes.cs index 4edc9268e5..9be0a8cf70 100644 --- a/Assets/SEE/Controls/Actions/ActionStateTypes.cs +++ b/Assets/SEE/Controls/Actions/ActionStateTypes.cs @@ -63,109 +63,109 @@ static ActionStateTypes() { Move = new("Move", "Move a node within a graph", - Color.red.Darker(), "Materials/Charts/MoveIcon", + Color.red.Darker(), Icons.Move, MoveAction.CreateReversibleAction); Rotate = new("Rotate", "Rotate the selected node and its children within a graph", - Color.blue.Darker(), "Materials/ModernUIPack/Refresh", + Color.blue.Darker(), Icons.Rotate, RotateAction.CreateReversibleAction); Hide = new("Hide", "Hides nodes or edges", - Color.yellow.Darker(), "Materials/ModernUIPack/Eye", + Color.yellow.Darker(), Icons.EyeSlash, HideAction.CreateReversibleAction); NewEdge = new("New Edge", "Draw a new edge between two nodes", - Color.green.Darker(), "Materials/ModernUIPack/Minus", + Color.green.Darker(), Icons.Edge, AddEdgeAction.CreateReversibleAction); NewNode = new("New Node", "Create a new node", - Color.green.Darker(), "Materials/ModernUIPack/Plus", + Color.green.Darker(), '+', AddNodeAction.CreateReversibleAction); EditNode = new("Edit Node", "Edit a node", - Color.green.Darker(), "Materials/ModernUIPack/Settings", + Color.green.Darker(), Icons.PenToSquare, EditNodeAction.CreateReversibleAction); ScaleNode = new("Scale Node", "Scale a node", - Color.green.Darker(), "Materials/ModernUIPack/Crop", + Color.green.Darker(), Icons.Scale, ScaleNodeAction.CreateReversibleAction); Delete = new("Delete", "Delete a node or an edge", - Color.yellow.Darker(), "Materials/ModernUIPack/Trash", + Color.yellow.Darker(), Icons.Trash, DeleteAction.CreateReversibleAction); ShowCode = new("Show Code", "Display the source code of a node.", - Color.black, "Materials/ModernUIPack/Document", + Color.black, Icons.Code, ShowCodeAction.CreateReversibleAction); Draw = new("Draw", "Draw a line", - Color.magenta.Darker(), "Materials/ModernUIPack/Pencil", + Color.magenta.Darker(), Icons.Pencil, DrawAction.CreateReversibleAction); AcceptDivergence = new("Accept Divergence", "Accept a diverging edge into the architecture", - Color.grey.Darker(), "Materials/ModernUIPack/Arrow Bold", + Color.grey.Darker(), Icons.CheckedCheckbox, AcceptDivergenceAction.CreateReversibleAction); // Metric Board actions MetricBoard = - new("Metric Board", "Manipulate a metric board", - Color.white.Darker(), "Materials/ModernUIPack/Pencil"); + new("Metric Board", "Manipulate a metric board", + Color.white.Darker(), Icons.Chalkboard); AddBoard = new("Add Board", "Add a board", - Color.green.Darker(), "Materials/ModernUIPack/Plus", + Color.green.Darker(), '+', AddBoardAction.CreateReversibleAction, parent: MetricBoard); AddWidget = new("Add Widget", "Add a widget", - Color.green.Darker(), "Materials/ModernUIPack/Plus", + Color.green.Darker(), '+', AddWidgetAction.CreateReversibleAction, parent: MetricBoard); MoveBoard = new("Move Board", "Move a board", - Color.yellow.Darker(), "Materials/Charts/MoveIcon", + Color.yellow.Darker(), Icons.Move, MoveBoardAction.CreateReversibleAction, parent: MetricBoard); MoveWidget = new("Move Widget", "Move a widget", - Color.yellow.Darker(), "Materials/Charts/MoveIcon", + Color.yellow.Darker(), Icons.Move, MoveWidgetAction.CreateReversibleAction, parent: MetricBoard); DeleteBoard = new("Delete Board", "Delete a board", - Color.red.Darker(), "Materials/ModernUIPack/Trash", + Color.red.Darker(), Icons.Trash, DeleteBoardAction.CreateReversibleAction, parent: MetricBoard); DeleteWidget = new("Delete Widget", "Delete a widget", - Color.red.Darker(), "Materials/ModernUIPack/Trash", + Color.red.Darker(), Icons.Trash, DeleteWidgetAction.CreateReversibleAction, parent: MetricBoard); LoadBoard = new("Load Board", "Load a board", - Color.blue.Darker(), "Materials/ModernUIPack/Document", + Color.blue.Darker(), Icons.Import, LoadBoardAction.CreateReversibleAction, parent: MetricBoard); SaveBoard = new("Save Board", "Save a board", - Color.blue.Darker(), "Materials/ModernUIPack/Document", + Color.blue.Darker(), Icons.Export, SaveBoardAction.CreateReversibleAction, parent: MetricBoard); } @@ -196,7 +196,7 @@ static ActionStateTypes() #endregion /// - /// Dumps all elements in - + /// Dumps all elements in . /// Can be used for debugging. /// public static void Dump() diff --git a/Assets/SEE/Controls/Actions/ShowCodeAction.cs b/Assets/SEE/Controls/Actions/ShowCodeAction.cs index e181ef46d0..b79db6d32b 100644 --- a/Assets/SEE/Controls/Actions/ShowCodeAction.cs +++ b/Assets/SEE/Controls/Actions/ShowCodeAction.cs @@ -9,10 +9,13 @@ using UnityEngine; using SEE.DataModel.DG; using System; +using Cysharp.Threading.Tasks; using SEE.UI.Window; using SEE.Utils.History; using SEE.Game.City; using SEE.VCS; +using GraphElementRef = SEE.GO.GraphElementRef; +using Range = SEE.DataModel.DG.Range; namespace SEE.Controls.Actions { @@ -213,7 +216,7 @@ public static CodeWindow ShowVCSDiff(GraphElementRef graphElementRef, DiffCity c { case Change.Unmodified or Change.Added or Change.TypeChanged or Change.Copied or Change.Unknown: // We can show the plain file in the newer revision. - codeWindow.EnterFromText(vcs.Show(relativePath, city.NewRevision).Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)); + codeWindow.EnterFromText(vcs.Show(relativePath, city.NewRevision).Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)); break; case Change.Modified or Change.Deleted or Change.Renamed: // If a file was renamed, it can still have differences. @@ -225,24 +228,47 @@ public static CodeWindow ShowVCSDiff(GraphElementRef graphElementRef, DiffCity c throw new Exception($"Unexpected change type {change} for {relativePath}"); } - switch (change) + codeWindow.Title = change switch { - case Change.Renamed: - codeWindow.Title = $"{oldRelativePath}" - + $" -> {sourceFilename}"; - break; - case Change.Deleted: - codeWindow.Title = $"{sourceFilename}"; - break; - default: - codeWindow.Title = sourceFilename; - break; - } + Change.Renamed => $"{oldRelativePath}" + + $" -> {sourceFilename}", + Change.Deleted => $"{sourceFilename}", + _ => sourceFilename + }; codeWindow.ScrolledVisibleLine = 1; return codeWindow; } + /// + /// Returns a CodeWindow showing the code range of the graph element most closely matching + /// the given and in the given . + /// Will return null and show an error message if no suitable graph element is found. + /// + /// The graph to search in + /// The path to search for + /// The range to search for + /// Action to be executed after the CodeWindow has been filled + /// with its content + /// new CodeWindow showing the code range of the graph element most closely matching + /// the given and + public static CodeWindow ShowCodeForPath(Graph graph, string path, Range range = null, Action ContentTextEntered = null) + { + // If we just have a path as input, we need to find a fitting graph element. + GraphElementRef element = graph.FittingElements(path, range).WithGameObject() + .Select(x => x.GameObject().MustGetComponent()) + .FirstOrDefault(); + + if (element == null) + { + ShowNotification.Error("No graph element found", + $"No suitable graph element found for path {path}", log: false); + return null; + } + + return ShowCode(element, ContentTextEntered); + } + /// /// Returns a CodeWindow showing the code range of the given graph element /// retrieved from a file. The path of the file is retrieved from @@ -250,40 +276,42 @@ public static CodeWindow ShowVCSDiff(GraphElementRef graphElementRef, DiffCity c /// attributes. /// /// The graph element to get the CodeWindow for + /// Action to be executed after the CodeWindow has been filled + /// with its content /// new CodeWindow showing the code range of the given graph element - public static CodeWindow ShowCode(GraphElementRef graphElementRef) + public static CodeWindow ShowCode(GraphElementRef graphElementRef, Action ContentTextEntered = null) { GraphElement graphElement = graphElementRef.Elem; - CodeWindow codeWindow; - if (graphElement.TryGetCommitID(out string commitID)) + CodeWindow codeWindow = GetOrCreateCodeWindow(graphElementRef, graphElement.Filename); + EnterWindowContent().ContinueWith(() => ContentTextEntered?.Invoke(codeWindow)); + return codeWindow; + + async UniTask EnterWindowContent() { - codeWindow = GetOrCreateCodeWindow(graphElementRef, graphElement.Filename); - if (!graphElement.TryGetRepositoryPath(out string repositoryPath)) + // We have to differentiate between a file-based and a VCS-based code city. + if (graphElement.TryGetCommitID(out string commitID)) { - string message = $"Selected {GetName(graphElement)} has no repository path."; - ShowNotification.Error("No repository path", message, log: false); - throw new InvalidOperationException(message); + if (!graphElement.TryGetRepositoryPath(out string repositoryPath)) + { + string message = $"Selected {GetName(graphElement)} has no repository path."; + ShowNotification.Error("No repository path", message, log: false); + throw new InvalidOperationException(message); + } + IVersionControl vcs = VersionControlFactory.GetVersionControl(VCSKind.Git, repositoryPath); + string[] fileContent = vcs.Show(graphElement.ID, commitID).Split("\\n", StringSplitOptions.RemoveEmptyEntries); + codeWindow.EnterFromText(fileContent); + } + else if (!codeWindow.ContainsText) + { + await codeWindow.EnterFromFileAsync(GetPath(graphElement).absolutePlatformPath); } - IVersionControl vcs = VersionControlFactory.GetVersionControl(VCSKind.Git, repositoryPath); - string[] fileContent = vcs.Show(graphElement.ID, commitID). - Split("\\n", StringSplitOptions.RemoveEmptyEntries); - codeWindow.EnterFromText(fileContent); - } - else - { - (string filename, string absolutePlatformPath) = GetPath(graphElement); - codeWindow = GetOrCreateCodeWindow(graphElementRef, filename); - // File name of source code file to read from it - codeWindow.EnterFromFile(absolutePlatformPath); - } - // Pass line number to automatically scroll to it, if it exists - if (graphElement.SourceLine is { } line) - { - codeWindow.ScrolledVisibleLine = line; + // Pass line number to automatically scroll to it, if it exists + if (graphElement.SourceLine is { } line) + { + codeWindow.ScrolledVisibleLine = line; + } } - - return codeWindow; } public override bool Update() @@ -299,6 +327,13 @@ public override bool Update() return false; } + ShowCodeWindow(); + } + + return false; + + void ShowCodeWindow() + { // Edges of type Clone will be handled differently. For these, we will be // showing a unified diff. CodeWindow codeWindow = graphElementRef is EdgeRef { Value: { Type: "Clone" } } edgeRef @@ -313,8 +348,6 @@ public override bool Update() manager.ActiveWindow = codeWindow; // TODO (#669): Set font size etc in settings (maybe, or maybe that's too much) } - - return false; } } } diff --git a/Assets/SEE/Controls/KeyActions/KeyBindings.cs b/Assets/SEE/Controls/KeyActions/KeyBindings.cs index 93d341f024..22975e39dd 100644 --- a/Assets/SEE/Controls/KeyActions/KeyBindings.cs +++ b/Assets/SEE/Controls/KeyActions/KeyBindings.cs @@ -12,7 +12,7 @@ internal static class KeyBindings { // IMPORTANT NOTES: // (1) Keep in mind that KeyCodes in Unity map directly to a - // physical key on an keyboard with an English layout. + // physical key on a keyboard with an English layout. // (2) Ctrl-Z and Ctrl-Y are reserved for Undo and Redo. // (3) The digits 0-9 are reserved for shortcuts for the player menu. @@ -30,6 +30,7 @@ internal static class KeyBindings /// /// Returns true if the user has pressed down a key requesting the given + /// in the last frame. /// /// the to check /// true if the user has pressed a key requesting the given diff --git a/Assets/SEE/Controls/WindowSpaceManager.cs b/Assets/SEE/Controls/WindowSpaceManager.cs index 1664232c94..3a3a8591fd 100644 --- a/Assets/SEE/Controls/WindowSpaceManager.cs +++ b/Assets/SEE/Controls/WindowSpaceManager.cs @@ -83,11 +83,11 @@ public void UpdateSpaceFromValueObject(string playerName, WindowSpace.WindowSpac { windowSpaces[playerName] = WindowSpace.FromValueObject(valueObject, gameObject); windowMenu.AddEntry(new MenuEntry(() => ActivateSpace(playerName), - DeactivateCurrentSpace, playerName, + playerName, DeactivateCurrentSpace, $"Code window from player with IP address '{playerName}'.", Color.white)); windowSpaces[playerName].WindowSpaceName += $" ({playerName})"; windowSpaces[playerName].enabled = false; - windowSpaces[playerName].CanClose = false; // User may only close their own windows + windowSpaces[playerName].CanClose = false; // User may only close their own windows } else { @@ -171,22 +171,21 @@ private void Update() /// private void SetUpWindowSelectionMenu() { - //TODO: Icons windowMenu = gameObject.AddComponent(); - MenuEntry localEntry = new(selectAction: () => ActivateSpace(LocalPlayer), - unselectAction: DeactivateCurrentSpace, - title: LocalPlayer, - description: "Windows for the local player (you).", - entryColor: Color.black); - MenuEntry noneEntry = new(() => CurrentPlayer = NoPlayer, () => { }, NoPlayer, - "This option hides all windows.", Color.grey); + MenuEntry localEntry = new(SelectAction: () => ActivateSpace(LocalPlayer), + UnselectAction: DeactivateCurrentSpace, + Title: LocalPlayer, + Description: "Windows for the local player (you).", + EntryColor: Color.black); + MenuEntry noneEntry = new(() => CurrentPlayer = NoPlayer, NoPlayer, + Description: "This option hides all windows.", EntryColor: Color.grey); windowMenu.AddEntry(noneEntry); windowMenu.AddEntry(localEntry); windowMenu.SelectEntry(localEntry); foreach (KeyValuePair space in windowSpaces.Where(space => space.Key != LocalPlayer)) { windowMenu.AddEntry(new MenuEntry(() => ActivateSpace(space.Key), - DeactivateCurrentSpace, space.Key, + space.Key, DeactivateCurrentSpace, $"Window from player with IP address '{space.Key}'.", Color.white)); } windowMenu.Title = "Window Selection"; diff --git a/Assets/SEE/DataModel/DG/GraphExtensions.cs b/Assets/SEE/DataModel/DG/GraphExtensions.cs index 35d1f41005..0a8eba3a2c 100644 --- a/Assets/SEE/DataModel/DG/GraphExtensions.cs +++ b/Assets/SEE/DataModel/DG/GraphExtensions.cs @@ -29,17 +29,17 @@ public static class GraphExtensions /// the elements in both graphs that have no differences according /// to ; it belongs to public static void Diff - (this Graph newGraph, - Graph oldGraph, - Func> getElements, - Func getElement, - IGraphElementDiff diff, - GraphElementEqualityComparer comparer, - out ISet added, - out ISet removed, - out ISet changed, - out ISet equal) - where T : GraphElement + (this Graph newGraph, + Graph oldGraph, + Func> getElements, + Func getElement, + IGraphElementDiff diff, + GraphElementEqualityComparer comparer, + out ISet added, + out ISet removed, + out ISet changed, + out ISet equal) + where T : GraphElement { IEnumerable oldElements = oldGraph != null ? getElements(oldGraph).ToList() : null; IEnumerable newElements = newGraph != null ? getElements(newGraph).ToList() : null; @@ -142,5 +142,52 @@ public static IEnumerable ApplyAll(this IEnumerable modifi { return modifiers.Aggregate(elements, (current, modifier) => modifier.Apply(current)); } + + /// + /// Returns the graph elements that most closely match the given + /// and , ordered by descending specificity. + /// + /// If the is not given, we prefer file nodes + /// (as they most closely represent the file at the given path), + /// + /// If the is given, we prefer elements that have a source range + /// which contains the given range. If multiple elements have a source range that contains + /// the given range, we prefer the one with the fewest lines. + /// + /// The graph to search in + /// The path to search for + /// The range to search for + /// The graph elements that most closely match the given path and range + public static IOrderedEnumerable FittingElements(this Graph graph, string path, Range range = null) + { + return graph.Elements().Where(e => e.Path() == path).OrderBy(OrderKey); + + int OrderKey(GraphElement graphElement) + { + if (range != null && graphElement.SourceRange != null && graphElement.SourceRange.Contains(range)) + { + // The fewer lines there are (i.e., the more specific the element is), the higher this is ordered. + // The 1_000_000 is just an arbitrary large number to make sure that the range is always preferred. + return graphElement.SourceRange.Lines - 1_000_000; + } + else + { + // We prefer file nodes (as they most closely represent the file at the given path), + // but fall back to using more specific node kinds, as long as they have the same path. + if (graphElement.Type == "File") + { + return 1; + } + else if (graphElement.SourceLine == null) + { + return 2; + } + else + { + return 3; + } + } + } + } } } diff --git a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs index 7a4b76f0cd..e4ee030187 100644 --- a/Assets/SEE/DataModel/DG/IO/LSPImporter.cs +++ b/Assets/SEE/DataModel/DG/IO/LSPImporter.cs @@ -6,10 +6,10 @@ using Cysharp.Threading.Tasks; using Markdig; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using OmniSharp.Extensions.LanguageServer.Server; using SEE.Tools; using SEE.Tools.LSP; using SEE.Utils; +using SEE.Utils.Markdown; using UnityEngine; using UnityEngine.Assertions; @@ -185,7 +185,7 @@ public async UniTask LoadAsync(Graph graph, Action changePercentage = nul CancellationToken token = default) { // Query all documents whose file extension is supported by the language server. - List relevantExtensions = Handler.Server.Languages.SelectMany(x => x.Extensions).ToList(); + List relevantExtensions = Handler.Server.Languages.SelectMany(x => x.FileExtensions).ToList(); List relevantDocuments = SourcePaths.SelectMany(RelevantDocumentsForPath) .Where(x => ExcludedPaths.All(y => !x.StartsWith(y))) .Distinct().ToList(); @@ -304,8 +304,7 @@ public async UniTask LoadAsync(Graph graph, Action changePercentage = nul } if (IncludeEdgeTypes.HasFlag(EdgeKind.Call) && Handler.ServerCapabilities.CallHierarchyProvider.TrueOrValue()) { - // FIXME (external: OmniSharp bug, sends wrong method name) - // await HandleCallHierarchyAsync(node, graph, token); + await HandleCallHierarchyAsync(node, graph, token); } if (IncludeEdgeTypes.HasFlag(EdgeKind.Extend) && Handler.ServerCapabilities.TypeHierarchyProvider.TrueOrValue()) { @@ -316,7 +315,6 @@ public async UniTask LoadAsync(Graph graph, Action changePercentage = nul // The Count+1 prevents the progress from reaching 1.0, since the diagnostics may not yet be pulled. changePercentage?.Invoke(1 - edgeProgressFactor + edgeProgressFactor * i++ / (relevantNodes.Count+1)); } - //Handler.CloseDocument(path); } Debug.Log($"LSPImporter: Imported {graph.Nodes().Except(originalNodes).Count()} new nodes and {newEdges} new edges.\n"); @@ -325,8 +323,8 @@ public async UniTask LoadAsync(Graph graph, Action changePercentage = nul { // In this case, we will wait one additional second to give the server at least some time to emit diagnostics. // TODO (#746): Collect diagnostics in background, or find a better way to handle this. - await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: token); - foreach (PublishDiagnosticsParams diagnosticsParams in Handler.GetPublishedDiagnostics()) + await UniTask.Delay(Handler.TimeoutSpan, cancellationToken: token); + foreach (PublishDiagnosticsParams diagnosticsParams in Handler.GetUnhandledPublishedDiagnostics()) { HandleDiagnostics(diagnosticsParams.Diagnostics, diagnosticsParams.Uri.Path); } @@ -337,6 +335,8 @@ public async UniTask LoadAsync(Graph graph, Action changePercentage = nul // Aggregate diagnostics upwards. We do this with a suffix, since these metrics may be used for erosion icons. MetricAggregator.AggregateSum(graph, IncludeDiagnostics.ToDiagnosticSeverity().Select(x => x.Name()), withSuffix: true, asInt: true); + graph.BasePath = Handler.ProjectPath; + changePercentage?.Invoke(1); return; @@ -387,14 +387,18 @@ private void HandleDiagnostics(IEnumerable diagnostics, string path) /// A cancellation token that can be used to cancel the operation. private async UniTask HandleCallHierarchyAsync(Node node, Graph graph, CancellationToken token) { - IUniTaskAsyncEnumerable results = Handler.OutgoingCalls(SelectItem, node.Path(), node.SourceLine ?? 0, node.SourceColumn ?? 0); + IUniTaskAsyncEnumerable results = Handler.OutgoingCalls(SelectItem, node.Path(), node.SourceLine - 1 ?? 0, node.SourceColumn - 1 ?? 0); await foreach (CallHierarchyItem item in results) { if (token.IsCancellationRequested) { throw new OperationCanceledException(); } - Node targetNode = FindNodesByLocation(item.Uri.Path, Range.FromLspRange(item.Range)).First(); + Node targetNode = FindNodesByLocation(item.Uri.Path, Range.FromLspRange(item.Range)).FirstOrDefault(); + if (targetNode == null) + { + continue; + } Edge edge = AddEdge(node, targetNode, LSP.Call, false, graph); edge.SetRange(SelectionRangeAttribute, Range.FromLspRange(item.SelectionRange)); } @@ -402,7 +406,7 @@ private async UniTask HandleCallHierarchyAsync(Node node, Graph graph, Cancellat bool SelectItem(CallHierarchyItem item) { - return item.Uri.Path == node.Path() && node.SourceRange.Contains(Range.FromLspRange(item.Range)); + return item.Uri.Path == node.Path() && Range.FromLspRange(item.Range) == node.SourceRange; } } @@ -415,14 +419,18 @@ bool SelectItem(CallHierarchyItem item) /// A cancellation token that can be used to cancel the operation. private async UniTask HandleTypeHierarchyAsync(Node node, Graph graph, CancellationToken token) { - IUniTaskAsyncEnumerable results = Handler.Supertypes(SelectItem, node.Path(), node.SourceLine ?? 0, node.SourceColumn ?? 0); + IUniTaskAsyncEnumerable results = Handler.Supertypes(SelectItem, node.Path(), node.SourceLine - 1 ?? 0, node.SourceColumn - 1 ?? 0); await foreach (TypeHierarchyItem item in results) { if (token.IsCancellationRequested) { throw new OperationCanceledException(); } - Node targetNode = FindNodesByLocation(item.Uri.Path, Range.FromLspRange(item.Range)).First(); + Node targetNode = FindNodesByLocation(item.Uri.Path, Range.FromLspRange(item.Range)).FirstOrDefault(); + if (targetNode == null) + { + continue; + } Edge edge = AddEdge(node, targetNode, LSP.Extend, false, graph); edge.SetRange(SelectionRangeAttribute, Range.FromLspRange(item.SelectionRange)); } @@ -494,7 +502,7 @@ private async UniTask AddSymbolNodeAsync(DocumentSymbol symbol, string path, Gra Hover hover = await Handler.HoverAsync(path, node.SourceLine - 1 ?? 0, node.SourceColumn - 1 ?? 0); if (hover != null) { - node.SetString("HoverText", MarkupToRichText(hover.Contents)); + node.SetString("HoverText", hover.Contents.ToRichText()); } } @@ -524,50 +532,6 @@ private async UniTask AddSymbolNodeAsync(DocumentSymbol symbol, string path, Gra } } - /// - /// Converts the given to TextMeshPro-compatible rich text. - /// - /// The content to convert. - /// The converted rich text. - private static string MarkupToRichText(MarkedStringsOrMarkupContent content) - { - string markdown; - if (content.HasMarkupContent) - { - MarkupContent markup = content.MarkupContent!; - switch (markup.Kind) - { - case MarkupKind.PlainText: return $"{markup.Value}"; - case MarkupKind.Markdown: - markdown = markup.Value; - break; - default: - Debug.LogError($"Unsupported markup kind: {markup.Kind}"); - return string.Empty; - } - } - else - { - // This is technically deprecated, but we still need to support it, - // since some language servers still use it. - Container strings = content.MarkedStrings!; - markdown = string.Join("\n", strings.Select(x => - { - if (x.Language != null) - { - return $"```{x.Language}\n{x.Value}\n```"; - } - else - { - return x.Value; - } - })); - } - - // TODO (#728): Parse markdown to TextMeshPro rich text (custom MarkDig parser). - return Markdown.ToPlainText(markdown); - } - /// /// Adds a node for the given to the given . /// If the node already exists, it is returned immediately. diff --git a/Assets/SEE/DataModel/DG/Range.cs b/Assets/SEE/DataModel/DG/Range.cs index 2467dc6698..8b223c3bf7 100644 --- a/Assets/SEE/DataModel/DG/Range.cs +++ b/Assets/SEE/DataModel/DG/Range.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using UnityEngine.Assertions; namespace SEE.DataModel.DG @@ -35,6 +36,45 @@ public record Range(int StartLine, int EndLine, int? StartCharacter = null, int? /// public (int Line, int Character) End => (EndLine, EndCharacter ?? 0); + /// + /// Whether this range has a character range. + /// If this is false, the range only contains full lines. + /// + public bool HasCharacter => StartCharacter.HasValue || EndCharacter.HasValue; + + /// + /// Splits this range into individual lines. + /// The resulting ranges, taken together, will cover the same area as this range, + /// but each range will only cover a single line at most. + /// + /// An enumerable of ranges, each covering a single line. + public IEnumerable SplitIntoLines() + { + if (Lines <= 1) + { + // Just the one line. + yield return this; + } + else if (HasCharacter) + { + // If we have characters, we need to split the first and last line. + yield return this with { EndLine = StartLine + 1, EndCharacter = null }; + for (int line = StartLine + 1; line < EndLine; line++) + { + yield return new Range(line, line + 1); + } + yield return this with { StartLine = EndLine - 1, StartCharacter = 0 }; + } + else + { + // If we don't have characters, we can just return the full lines. + for (int line = StartLine; line < EndLine; line++) + { + yield return new Range(line, line + 1); + } + } + } + /// /// Returns true if the given line and character are contained in this range. /// @@ -91,6 +131,16 @@ public bool Contains(Range other) return contains; } + /// + /// Whether this range overlaps with the given line. + /// + /// The line to check. + /// True if this range overlaps with the given line. + public bool Overlaps(int line) + { + return line >= StartLine && line < EndLine; + } + public override string ToString() { return StartCharacter.HasValue && EndCharacter.HasValue diff --git a/Assets/SEE/Game/City/ErosionAttributes.cs b/Assets/SEE/Game/City/ErosionAttributes.cs index 90f74120a9..cc55803875 100644 --- a/Assets/SEE/Game/City/ErosionAttributes.cs +++ b/Assets/SEE/Game/City/ErosionAttributes.cs @@ -3,6 +3,7 @@ using SEE.DataModel.DG; using SEE.Utils.Config; using UnityEngine; +using UnityEngine.Serialization; namespace SEE.Game.City { @@ -34,9 +35,10 @@ public class ErosionAttributes : VisualAttributes public float ErosionScalingFactor = 1.5f; /// - /// Whether code issues should be downloaded and shown in code viewers. + /// Whether code issues from the Axivion Dashboard should be downloaded and shown in code viewers. /// - public bool ShowIssuesInCodeWindow = false; + [FormerlySerializedAs("ShowIssuesInCodeWindow")] + public bool ShowDashboardIssuesInCodeWindow = false; /// /// The attribute name of the metric representing architecture violations. @@ -129,7 +131,7 @@ public override void Save(ConfigWriter writer, string label) writer.BeginGroup(label); writer.Save(ShowInnerErosions, showInnerErosionsLabel); writer.Save(ShowLeafErosions, showLeafErosionsLabel); - writer.Save(ShowIssuesInCodeWindow, showIssuesInCodeWindowLabel); + writer.Save(ShowDashboardIssuesInCodeWindow, showIssuesInCodeWindowLabel); writer.Save(ErosionScalingFactor, erosionScalingFactorLabel); writer.Save(StyleIssue, styleIssueLabel); @@ -162,7 +164,7 @@ public override void Restore(Dictionary attributes, string label ConfigIO.Restore(values, showInnerErosionsLabel, ref ShowInnerErosions); ConfigIO.Restore(values, showLeafErosionsLabel, ref ShowLeafErosions); - ConfigIO.Restore(values, showIssuesInCodeWindowLabel, ref ShowIssuesInCodeWindow); + ConfigIO.Restore(values, showIssuesInCodeWindowLabel, ref ShowDashboardIssuesInCodeWindow); ConfigIO.Restore(values, erosionScalingFactorLabel, ref ErosionScalingFactor); ConfigIO.Restore(values, styleIssueLabel, ref StyleIssue); @@ -190,7 +192,7 @@ public override void Restore(Dictionary attributes, string label private const string showLeafErosionsLabel = "ShowLeafErosions"; private const string showInnerErosionsLabel = "ShowInnerErosions"; private const string erosionScalingFactorLabel = "ErosionScalingFactor"; - private const string showIssuesInCodeWindowLabel = "ShowIssuesInCodeWindow"; + private const string showIssuesInCodeWindowLabel = "ShowDashboardIssuesInCodeWindow"; private const string styleIssueLabel = "StyleIssue"; private const string universalIssueLabel = "UniversalIssue"; diff --git a/Assets/SEE/Game/City/SEECity.cs b/Assets/SEE/Game/City/SEECity.cs index c9bd455a0a..54ed2144fa 100644 --- a/Assets/SEE/Game/City/SEECity.cs +++ b/Assets/SEE/Game/City/SEECity.cs @@ -305,7 +305,7 @@ public virtual async UniTask LoadDataAsync() { try { - using (LoadingSpinner.ShowDeterminate($"Loading city \"{gameObject.name}\"...\n", + using (LoadingSpinner.ShowDeterminate($"Loading city \"{gameObject.name}\"...", out Action reportProgress)) { void ReportProgress(float x) diff --git a/Assets/SEE/GameObjects/Menu/PlayerMenu.cs b/Assets/SEE/GameObjects/Menu/PlayerMenu.cs index 24cf11407e..4331bc8f85 100644 --- a/Assets/SEE/GameObjects/Menu/PlayerMenu.cs +++ b/Assets/SEE/GameObjects/Menu/PlayerMenu.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using InControl; using SEE.Controls; using SEE.Controls.Actions; using SEE.Controls.KeyActions; @@ -86,12 +85,11 @@ bool Visit(AbstractActionStateType child, AbstractActionStateType parent) if (child is ActionStateType actionStateType) { - MenuEntry menuEntry = new(selectAction: () => GlobalActionHistory.Execute(actionStateType), - unselectAction: null, - title: actionStateType.Name, - description: actionStateType.Description, - entryColor: actionStateType.Color, - icon: Resources.Load(actionStateType.IconPath)); + MenuEntry menuEntry = new(SelectAction: () => GlobalActionHistory.Execute(actionStateType), + Title: actionStateType.Name, + Description: actionStateType.Description, + EntryColor: actionStateType.Color, + Icon: actionStateType.Icon); entry = menuEntry; } else if (child is ActionStateTypeGroup actionStateTypeGroup) @@ -100,7 +98,7 @@ bool Visit(AbstractActionStateType child, AbstractActionStateType parent) title: actionStateTypeGroup.Name, description: actionStateTypeGroup.Description, entryColor: actionStateTypeGroup.Color, - icon: Resources.Load(actionStateTypeGroup.IconPath)); + icon: actionStateTypeGroup.Icon); toNestedMenuEntry[actionStateTypeGroup] = nestedMenuEntry; entry = nestedMenuEntry; } diff --git a/Assets/SEE/GraphProviders/GxlGraphProvider.cs.meta b/Assets/SEE/GraphProviders/GXLGraphProvider.cs.meta similarity index 100% rename from Assets/SEE/GraphProviders/GxlGraphProvider.cs.meta rename to Assets/SEE/GraphProviders/GXLGraphProvider.cs.meta diff --git a/Assets/SEE/GraphProviders/LSPGraphProvider.cs b/Assets/SEE/GraphProviders/LSPGraphProvider.cs index 926a3f2127..d07e86432f 100644 --- a/Assets/SEE/GraphProviders/LSPGraphProvider.cs +++ b/Assets/SEE/GraphProviders/LSPGraphProvider.cs @@ -105,6 +105,13 @@ public class LSPGraphProvider : SingleGraphProvider [EnumToggleButtons, FoldoutGroup("Import Settings")] public DiagnosticKind IncludedDiagnosticLevels = DiagnosticKind.All; + /// + /// If true, LSP functionality will be available in code windows. + /// + [Tooltip("If true, LSP functionality will be available in code windows."), RuntimeTab(GraphProviderFoldoutGroup)] + [LabelWidth(150)] + public bool UseInCodeWindows = true; + /// /// If true, the communication between the language server and SEE will be logged. /// @@ -139,7 +146,7 @@ public class LSPGraphProvider : SingleGraphProvider /// The available language servers as a dropdown list. private IEnumerable ServerDropdown() { - return LSPLanguage.All.Select(language => (language, LSPServer.All.Where(server => server.Languages.Contains(language)))) + return LSPLanguage.AllLspLanguages.Select(language => (language, LSPServer.All.Where(server => server.Languages.Contains(language)))) .SelectMany(pair => pair.Item2.Select(server => $"{pair.language}/{server}")) .OrderBy(server => server); } @@ -248,6 +255,7 @@ public override async UniTask ProvideAsync(Graph graph, AbstractSEECity c handler.Server = Server; handler.ProjectPath = ProjectPath.Path; handler.LogLSP = LogLSP; + handler.UseInCodeWindows = UseInCodeWindows; handler.TimeoutSpan = TimeSpan.FromSeconds(Timeout); await handler.InitializeAsync(executablePath: ServerPath ?? Server.ServerExecutable, token); if (token.IsCancellationRequested) diff --git a/Assets/SEE/GraphProviders/VCSGraphProvider.cs b/Assets/SEE/GraphProviders/VCSGraphProvider.cs index 7c75ec9459..931bce08be 100644 --- a/Assets/SEE/GraphProviders/VCSGraphProvider.cs +++ b/Assets/SEE/GraphProviders/VCSGraphProvider.cs @@ -15,6 +15,7 @@ using SEE.Scanner; using System.Threading; using Microsoft.Extensions.FileSystemGlobbing; +using SEE.Scanner.Antlr; namespace SEE.GraphProviders { @@ -264,7 +265,7 @@ public static Node BuildGraphFromPath(string path, Node parent, string parentPat } // Directory does not exist. - if (currentSegmentNode == null && pathSegments.Length > 1 && parent == null) + if (pathSegments.Length > 1 && parent == null) { rootNode.AddChild(NewNode(graph, pathSegments[0], directoryNodeType, pathSegments[0])); return BuildGraphFromPath(nodePath, graph.GetNode(pathSegments[0]), @@ -286,8 +287,7 @@ public static Node BuildGraphFromPath(string path, Node parent, string parentPat } // The node for the current pathSegment does not exist, and the node is a directory. - if (currentPathSegmentNode == null && - pathSegments.Length > 1) + if (pathSegments.Length > 1) { parent.AddChild(NewNode(graph, currentPathSegment, directoryNodeType, pathSegments[0])); return BuildGraphFromPath(nodePath, graph.GetNode(currentPathSegment), @@ -295,8 +295,7 @@ public static Node BuildGraphFromPath(string path, Node parent, string parentPat } // The node for the current pathSegment does not exist, and the node is a file. - if (currentPathSegmentNode == null && - pathSegments.Length == 1) + if (pathSegments.Length == 1) { Node addedFileNode = NewNode(graph, currentPathSegment, fileNodeType, pathSegments[0]); parent.AddChild(addedFileNode); @@ -342,21 +341,21 @@ private static List ListTree(LibGit2Sharp.Tree tree) /// The commitID where the files exist. /// The language the given text is written in. /// The token stream for the specified file and commit. - public static IEnumerable RetrieveTokens(string repositoryFilePath, Repository repository, - string commitID, TokenLanguage language) + public static ICollection RetrieveTokens(string repositoryFilePath, Repository repository, + string commitID, AntlrLanguage language) { Blob blob = repository.Lookup($"{commitID}:{repositoryFilePath}"); if (blob != null) { string fileContent = blob.GetContentText(); - return SEEToken.FromString(fileContent, language); + return AntlrToken.FromString(fileContent, language); } else { // Blob does not exist. Debug.LogWarning($"File {repositoryFilePath} does not exist.\n"); - return Enumerable.Empty(); + return new List(); } } @@ -375,10 +374,10 @@ private static void AddCodeMetrics(Graph graph, Repository repository, string co if (node.Type == fileNodeType) { string repositoryFilePath = node.ID; - TokenLanguage language = TokenLanguage.FromFileExtension(Path.GetExtension(repositoryFilePath).TrimStart('.')); - if (language != TokenLanguage.Plain) + AntlrLanguage language = AntlrLanguage.FromFileExtension(Path.GetExtension(repositoryFilePath).TrimStart('.')); + if (language != AntlrLanguage.Plain) { - IEnumerable tokens = RetrieveTokens(repositoryFilePath, repository, commitID, language); + ICollection tokens = RetrieveTokens(repositoryFilePath, repository, commitID, language); node.SetInt(Metrics.Prefix + "LOC", TokenMetrics.CalculateLinesOfCode(tokens)); node.SetInt(Metrics.Prefix + "McCabe_Complexity", TokenMetrics.CalculateMcCabeComplexity(tokens)); TokenMetrics.HalsteadMetrics halsteadMetrics = TokenMetrics.CalculateHalsteadMetrics(tokens); diff --git a/Assets/SEE/Net/Dashboard/DashboardException.cs b/Assets/SEE/Net/Dashboard/DashboardException.cs index 6adc3dd3f2..3491df3d54 100644 --- a/Assets/SEE/Net/Dashboard/DashboardException.cs +++ b/Assets/SEE/Net/Dashboard/DashboardException.cs @@ -27,7 +27,7 @@ public DashboardException(DashboardError error) : this($"{error.Type}: {error.Lo /// Instantiates a new with the given exception. /// /// Exception which occurred when accessing the dashboard API. - public DashboardException(Exception inner) : this("An error occurred while retrieving dashboard data.", inner) + public DashboardException(Exception inner) : this($"An error occurred while retrieving dashboard data ({inner.Message}).", inner) { // Intentionally empty, the other constructor that's being called is already doing all the work. } @@ -46,4 +46,4 @@ private DashboardException(string message, Exception inner) : base(message, inne #endregion } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/DashboardResult.cs b/Assets/SEE/Net/Dashboard/DashboardResult.cs index a41b94bf25..f0c169e388 100644 --- a/Assets/SEE/Net/Dashboard/DashboardResult.cs +++ b/Assets/SEE/Net/Dashboard/DashboardResult.cs @@ -15,7 +15,7 @@ public class DashboardResult /// /// Whether the API call has been successful. /// - public bool Success { get; private set; } + public bool Success { get; private init; } /// /// This contains the error object which has been returned from the dashboard, if the call was not successful @@ -115,7 +115,7 @@ public T RetrieveObject(bool strict = true) MissingMemberHandling = strict ? MissingMemberHandling.Error : MissingMemberHandling.Ignore }); } - catch (Exception e) when (e is JsonSerializationException || e is JsonReaderException) + catch (Exception e) when (e is JsonSerializationException or JsonReaderException) { Debug.LogError($"Error encountered: {e.Message}.\nGiven JSON was: {JSON}"); throw; @@ -131,4 +131,4 @@ public override string ToString() return $"{nameof(Error)}: {Error}, {nameof(Exception)}: {Exception}, {nameof(JSON)}: {JSON}, {nameof(Success)}: {Success}"; } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/DashboardRetriever.cs b/Assets/SEE/Net/Dashboard/DashboardRetriever.cs index 433a189a3d..be217b90dd 100644 --- a/Assets/SEE/Net/Dashboard/DashboardRetriever.cs +++ b/Assets/SEE/Net/Dashboard/DashboardRetriever.cs @@ -76,6 +76,16 @@ public partial class DashboardRetriever : MonoBehaviour [Tooltip("Whether to throw an error if Dashboard models have more fields than the C# models.")] public bool StrictMode = false; + /// + /// The number of seconds after which a request to the dashboard will be considered timed out. + /// If this is set to 0, the request will never time out. + /// + [EnvironmentVariable("DASHBOARD_TIMEOUT_SECONDS")] + [Tooltip("The number of seconds after which a request to the dashboard will be considered timed out. " + + "If this is set to 0, the request will never time out.")] + [Min(0)] + public float TimeoutSeconds = 5; + [Header("Issue Retrieval")] /// /// Whether s shall be retrieved when calling . @@ -159,7 +169,7 @@ private async UniTask GetAtPathAsync(string path, Dictionary 0) + if (queryParameters is { Count: > 0 }) { requestUrl += "?" + Encoding.UTF8.GetString(UnityWebRequest.SerializeSimpleForm(queryParameters)); } @@ -172,10 +182,13 @@ private async UniTask GetAtPathAsync(string path, Dictionary request.isDone); + request.SendWebRequest(); + bool timeout = !await AsyncUtils.RunWithTimeoutAsync(t => UniTask.WaitUntil(() => request.isDone, cancellationToken: t), + TimeSpan.FromSeconds(TimeoutSeconds), throwOnTimeout: false); + if (timeout) + { + return new DashboardResult(new TimeoutException("Request timed out.")); + } DashboardResult result = request.result switch { UnityWebRequest.Result.Success => new DashboardResult(true, request.downloadHandler.text), @@ -264,7 +277,7 @@ private async UniTaskVoid VerifyVersionNumberAsync() ShowNotification.Error("Dashboard Version too old", $"The version of the dashboard is {version}, but the DashboardRetriever " + $"has been written for version {DashboardVersion.SupportedVersion}." - + $" Please update your dashboard."); + + " Please update your dashboard."); break; case DashboardVersion.Difference.PathOlder: // If patch version is older, there may be some bugfixes / security problems not accounted for. @@ -310,12 +323,12 @@ private async UniTaskVoid VerifyVersionNumberAsync() public Color GetIssueColor(Issue issue) => issue switch { - ArchitectureViolationIssue _ => ArchitectureViolationIssueColor, - CloneIssue _ => CloneIssueColor, - CycleIssue _ => CycleIssueColor, - DeadEntityIssue _ => DeadEntityIssueColor, - MetricViolationIssue _ => MetricViolationIssueColor, - StyleViolationIssue _ => StyleViolationIssueColor, + ArchitectureViolationIssue => ArchitectureViolationIssueColor, + CloneIssue => CloneIssueColor, + CycleIssue => CycleIssueColor, + DeadEntityIssue => DeadEntityIssueColor, + MetricViolationIssue => MetricViolationIssueColor, + StyleViolationIssue => StyleViolationIssueColor, _ => throw new ArgumentOutOfRangeException(nameof(issue), issue, "Unknown issue kind!") }; diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/ArchitectureViolationIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/ArchitectureViolationIssue.cs index be94c1d549..b7c41ef28b 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/ArchitectureViolationIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/ArchitectureViolationIssue.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -143,25 +143,25 @@ protected ArchitectureViolationIssue(string architectureSource, string architect string targetEntityType, string targetPath, int targetLine, string targetLinkName) { - this.ArchitectureSource = architectureSource; - this.ArchitectureSourceType = architectureSourceType; - this.ArchitectureSourceLinkName = architectureSourceLinkName; - this.ArchitectureTarget = architectureTarget; - this.ArchitectureTargetType = architectureTargetType; - this.ArchitectureTargetLinkName = architectureTargetLinkName; - this.ErrorNumber = errorNumber; - this.ViolationType = violationType; - this.DependencyType = dependencyType; - this.SourceEntity = sourceEntity; - this.SourceEntityType = sourceEntityType; - this.SourcePath = sourcePath; - this.SourceLine = sourceLine; - this.SourceLinkName = sourceLinkName; - this.TargetEntity = targetEntity; - this.TargetEntityType = targetEntityType; - this.TargetPath = targetPath; - this.TargetLine = targetLine; - this.TargetLinkName = targetLinkName; + ArchitectureSource = architectureSource; + ArchitectureSourceType = architectureSourceType; + ArchitectureSourceLinkName = architectureSourceLinkName; + ArchitectureTarget = architectureTarget; + ArchitectureTargetType = architectureTargetType; + ArchitectureTargetLinkName = architectureTargetLinkName; + ErrorNumber = errorNumber; + ViolationType = violationType; + DependencyType = dependencyType; + SourceEntity = sourceEntity; + SourceEntityType = sourceEntityType; + SourcePath = sourcePath; + SourceLine = sourceLine; + SourceLinkName = sourceLinkName; + TargetEntity = targetEntity; + TargetEntityType = targetEntityType; + TargetPath = targetPath; + TargetLine = targetLine; + TargetLinkName = targetLinkName; } public override async UniTask ToDisplayStringAsync() @@ -187,4 +187,4 @@ public override async UniTask ToDisplayStringAsync() }.Where(x => x.path != null) .Select(x => new SourceCodeEntity(x.path, x.line, null, x.entity)); } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/CloneIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/CloneIssue.cs index b7db44f2bc..c412b10f59 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/CloneIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/CloneIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -89,17 +89,17 @@ public CloneIssue(int cloneType, string leftPath, int leftLine, int leftEndLine, int leftWeight, string rightPath, int rightLine, int rightEndLine, int rightLength, int rightWeight) { - this.CloneType = cloneType; - this.LeftPath = leftPath; - this.LeftLine = leftLine; - this.LeftEndLine = leftEndLine; - this.LeftLength = leftLength; - this.LeftWeight = leftWeight; - this.RightPath = rightPath; - this.RightLine = rightLine; - this.RightEndLine = rightEndLine; - this.RightLength = rightLength; - this.RightWeight = rightWeight; + CloneType = cloneType; + LeftPath = leftPath; + LeftLine = leftLine; + LeftEndLine = leftEndLine; + LeftLength = leftLength; + LeftWeight = leftWeight; + RightPath = rightPath; + RightLine = rightLine; + RightEndLine = rightEndLine; + RightLength = rightLength; + RightWeight = rightWeight; } public override async UniTask ToDisplayStringAsync() diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/CycleIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/CycleIssue.cs index 0517f977ad..9afcdf137a 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/CycleIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/CycleIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -89,17 +89,17 @@ protected CycleIssue(string dependencyType, string sourceEntity, string sourceEn string sourcePath, int sourceLine, string sourceLinkName, string targetEntity, string targetEntityType, string targetPath, int targetLine, string targetLinkName) { - this.DependencyType = dependencyType; - this.SourceEntity = sourceEntity; - this.SourceEntityType = sourceEntityType; - this.SourcePath = sourcePath; - this.SourceLine = sourceLine; - this.SourceLinkName = sourceLinkName; - this.TargetEntity = targetEntity; - this.TargetEntityType = targetEntityType; - this.TargetPath = targetPath; - this.TargetLine = targetLine; - this.TargetLinkName = targetLinkName; + DependencyType = dependencyType; + SourceEntity = sourceEntity; + SourceEntityType = sourceEntityType; + SourcePath = sourcePath; + SourceLine = sourceLine; + SourceLinkName = sourceLinkName; + TargetEntity = targetEntity; + TargetEntityType = targetEntityType; + TargetPath = targetPath; + TargetLine = targetLine; + TargetLinkName = targetLinkName; } public override async UniTask ToDisplayStringAsync() @@ -121,4 +121,4 @@ public override async UniTask ToDisplayStringAsync() new SourceCodeEntity(TargetPath, TargetLine, null, TargetEntity) }; } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/DeadEntityIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/DeadEntityIssue.cs index 89e444be7e..d01d96d303 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/DeadEntityIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/DeadEntityIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -51,11 +51,11 @@ public DeadEntityIssue() [JsonConstructor] protected DeadEntityIssue(string entity, string entityType, string path, int line, string linkName) { - this.Entity = entity; - this.EntityType = entityType; - this.Path = path; - this.Line = line; - this.LinkName = linkName; + Entity = entity; + EntityType = entityType; + Path = path; + Line = line; + LinkName = linkName; } public override async UniTask ToDisplayStringAsync() @@ -75,4 +75,4 @@ public override async UniTask ToDisplayStringAsync() new SourceCodeEntity(Path, Line, null, Entity) }; } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs index a2df91d371..959eb1fa63 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/Issue.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using Cysharp.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using SEE.DataModel.DG; +using SEE.UI.Window.CodeWindow; +using UnityEngine; +using Range = SEE.DataModel.DG.Range; namespace SEE.Net.Dashboard.Model.Issues { @@ -11,7 +16,7 @@ namespace SEE.Net.Dashboard.Model.Issues /// Contains information about an issue in the source code. /// [Serializable] - public abstract class Issue + public abstract class Issue : IDisplayableIssue { // A note: Due to how the JSON serializer works with inheritance, fields in here can't be readonly. @@ -24,22 +29,22 @@ public enum IssueState } /// - /// A kind-wide Id identifying the issue across analysis versions + /// A kind-wide ID identifying the issue across analysis versions /// [JsonProperty(PropertyName = "id", Required = Required.Always)] public int ID; /// /// In diff-queries, this indicates whether the issue is “Removed”, - /// i.e. contained in the base-version but not any more in the current version or “Added”, - /// i.e. it was not contained in the base-version but is contained in the current version + /// i.e. contained in the base-version but not anymore in the current version or “Added”, + /// that is, it was not contained in the base-version but is contained in the current version. /// [JsonConverter(typeof(StringEnumConverter))] [JsonProperty(PropertyName = "state", Required = Required.Default)] public IssueState State; /// - /// Whether or not the issue is suppressed or disabled via a control comment. + /// Whether the issue is suppressed or disabled via a control comment. /// /// /// This column is only available for projects where importing of suppressed issues is configured @@ -71,7 +76,7 @@ public enum IssueState /// /// The dashboard users associated with the issue via VCS blaming, CI path mapping, - /// CI user name mapping and dashboard user name mapping. + /// CI username mapping and dashboard username mapping. /// [JsonProperty(PropertyName = "owners", Required = Required.Always)] public IList Owners; @@ -81,8 +86,8 @@ protected Issue() // Necessary for inheritance with Newtonsoft.Json to work properly } - public Issue(int id, IssueState state, bool suppressed, string justification, - IList tag, IList comments, IList owners) + protected Issue(int id, IssueState state, bool suppressed, string justification, + IList tag, IList comments, IList owners) { ID = id; State = state; @@ -126,7 +131,7 @@ public IssueTag(string tag, string color) public readonly struct IssueComment { /// - /// The loginname of the user that created the comment. + /// The login name of the user that created the comment. /// [JsonProperty(PropertyName = "username", Required = Required.Always)] public readonly string Username; @@ -207,5 +212,47 @@ public IssueComment(string username, string userDisplayName, DateTime date, /// May be empty if all referenced entities don't have a path. /// public abstract IEnumerable Entities { get; } + + /// + /// The color to use when marking this issue in code windows. + /// + public Color Color => DashboardRetriever.Instance.GetIssueColor(this); + + public IList RichTags => new List + { + // Use a transparency value of 0x33 + $"" + }; + + /// + /// Implements . + /// + public string Source => "Axivion Dashboard"; + + /// + /// Implements . + /// + public IEnumerable<(string Path, Range Range)> Occurrences => Entities.Select(e => (e.Path, new Range(e.Line, (e.EndLine ?? e.Line) + 1))); + + /// + /// Implements . + /// + public (int startCharacter, int endCharacter)? GetCharacterRangeForLine(string path, int lineNumber, string line) + { + // Axivion Dashboard doesn't provide character ranges for issues, so we have to calculate them ourselves. + SourceCodeEntity entity = Entities.FirstOrDefault(e => e.Path == path && e.Line == lineNumber); + if (entity != null) + { + MatchCollection matches = Regex.Matches(line, Regex.Escape(entity.Content)); + // We return null if we found more than one occurence too, because in that case + // we have no way to determine which of the occurrences is the right one. + if (matches.Count == 1) + { + Match match = matches[0]; + return (match.Index, match.Index + match.Length); + } + } + return null; + } } } diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/MetricViolationIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/MetricViolationIssue.cs index 04d5a29f68..cc4ffdd19f 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/MetricViolationIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/MetricViolationIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -101,18 +101,18 @@ public MetricViolationIssue(string severity, string entity, string entityType, s string linkName, string metric, string errorNumber, string description, float? max, float? min, float value) { - this.Severity = severity; - this.Entity = entity; - this.EntityType = entityType; - this.Path = path; - this.Line = line; - this.LinkName = linkName; - this.Metric = metric; - this.ErrorNumber = errorNumber; - this.Description = description; - this.Max = max; - this.Min = min; - this.Value = value; + Severity = severity; + Entity = entity; + EntityType = entityType; + Path = path; + Line = line; + LinkName = linkName; + Metric = metric; + ErrorNumber = errorNumber; + Description = description; + Max = max; + Min = min; + Value = value; } public override async UniTask ToDisplayStringAsync() @@ -137,4 +137,4 @@ public override async UniTask ToDisplayStringAsync() new SourceCodeEntity(Path, Line, null, Entity) }; } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/SourceCodeEntity.cs b/Assets/SEE/Net/Dashboard/Model/Issues/SourceCodeEntity.cs index ceb5a21857..e0aaabc18c 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/SourceCodeEntity.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/SourceCodeEntity.cs @@ -35,10 +35,10 @@ public class SourceCodeEntity public SourceCodeEntity(string path, int line, int? endLine = null, string content = null) { - this.Path = path ?? throw new ArgumentNullException(nameof(path)); - this.Line = line; - this.EndLine = endLine; - this.Content = string.IsNullOrEmpty(content) ? null : content; + Path = path ?? throw new ArgumentNullException(nameof(path)); + Line = line; + EndLine = endLine; + Content = string.IsNullOrEmpty(content) ? null : content; } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Net/Dashboard/Model/Issues/StyleViolationIssue.cs b/Assets/SEE/Net/Dashboard/Model/Issues/StyleViolationIssue.cs index 70ada5b824..05d24ed39d 100644 --- a/Assets/SEE/Net/Dashboard/Model/Issues/StyleViolationIssue.cs +++ b/Assets/SEE/Net/Dashboard/Model/Issues/StyleViolationIssue.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; +using Newtonsoft.Json; using SEE.DataModel.DG; using SEE.Utils; -using Newtonsoft.Json; namespace SEE.Net.Dashboard.Model.Issues { @@ -64,13 +64,13 @@ public StyleViolationIssue() public StyleViolationIssue(string severity, string provider, string errorNumber, string message, string entity, string path, int line) { - this.Severity = severity; - this.Provider = provider; - this.ErrorNumber = errorNumber; - this.Message = message; - this.Entity = entity; - this.Path = path; - this.Line = line; + Severity = severity; + Provider = provider; + ErrorNumber = errorNumber; + Message = message; + Entity = entity; + Path = path; + Line = line; } public override async UniTask ToDisplayStringAsync() @@ -88,4 +88,4 @@ public override async UniTask ToDisplayStringAsync() new SourceCodeEntity(Path, Line, null, Entity) }; } -} \ No newline at end of file +} diff --git a/Assets/SEE/SEE.asmdef b/Assets/SEE/SEE.asmdef index ff55935359..da662f513f 100644 --- a/Assets/SEE/SEE.asmdef +++ b/Assets/SEE/SEE.asmdef @@ -66,7 +66,8 @@ "Markdig.dll", "System.Collections.Immutable.dll", "MoreLinq.dll", - "Microsoft.Extensions.FileSystemGlobbing.dll" + "Microsoft.Extensions.FileSystemGlobbing.dll", + "MediatR.dll" ], "autoReferenced": false, "defineConstraints": [], diff --git a/Assets/SEE/Scanner/Antlr.meta b/Assets/SEE/Scanner/Antlr.meta new file mode 100644 index 0000000000..9f35e31234 --- /dev/null +++ b/Assets/SEE/Scanner/Antlr.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 16346cc9b9ec430a9dfc96704b3868e2 +timeCreated: 1719432794 \ No newline at end of file diff --git a/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs b/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs new file mode 100644 index 0000000000..246bda12d8 --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs @@ -0,0 +1,582 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Antlr4.Runtime; + +namespace SEE.Scanner.Antlr +{ + /// + /// An Antlr-supported programming language a is in. + /// Symbolic names for the Antlr lexer are specified here. + /// + public class AntlrLanguage : TokenLanguage + { + /// + /// Name of the antlr lexer file the keywords were taken from. + /// + private string LexerFileName { get; } + + /// + /// Symbolic names for comments of a language, including block, line, and documentation comments. + /// + public ISet Comments { get; } + + /// + /// Symbolic names for keywords of a language. This also includes boolean literals and null literals. + /// + public ISet Keywords { get; } + + /// + /// Symbolic names for branch keywords of a language. + /// + public ISet BranchKeywords { get; } + + /// + /// Symbolic names for number literals of a language. This includes integer literals, floating point literals, etc. + /// + public ISet NumberLiterals { get; } + + /// + /// Symbolic names for string literals of a language. Also includes character literals. + /// + public ISet StringLiterals { get; } + + /// + /// Symbolic names for separators and operators of a language. + /// + public ISet Punctuation { get; } + + /// + /// Symbolic names for identifiers in a language. + /// + public ISet Identifiers { get; } + + /// + /// Symbolic names for whitespace in a language, excluding newlines. + /// + public ISet Whitespace { get; } + + /// + /// Symbolic names for newlines in a language. + /// + public ISet Newline { get; } + + #region Java Language + + /// + /// Name of the Java antlr grammar lexer. + /// + private const string javaFileName = "Java9Lexer.g4"; + + /// + /// Set of java file extensions. + /// + private static readonly HashSet javaExtensions = new() + { + "java" + }; + + /// + /// Set of antlr type names for Java keywords excluding . + /// + private static readonly HashSet javaKeywords = new() + { + "ABSTRACT", "ASSERT", "BOOLEAN", "BREAK", "BYTE", "CASE", "CATCH", "CHAR", "CLASS", "CONST", "CONTINUE", + "DEFAULT", "DO", "DOUBLE", "ELSE", "ENUM", "EXPORTS", "EXTENDS", "FINAL", "FINALLY", "FLOAT", + "GOTO", "IMPLEMENTS", "IMPORT", "INSTANCEOF", "INT", "INTERFACE", "LONG", "MODULE", "NATIVE", "NEW", + "OPEN", "OPERNS", "PACKAGE", "PRIVATE", "PROTECTED", "PROVIDES", "PUBLIC", "REQUIRES", "RETURN", "SHORT", + "STATIC", "STRICTFP", "SUPER", "SYNCHRONIZED", "THIS", "THROW", "THROWS", "TO", "TRANSIENT", + "TRANSITIVE", "USES", "VOID", "VOLATILE", "WITH", "UNDER_SCORE", + "BooleanLiteral", "NullLiteral" + }; + + /// + /// Set of antlr type names for Java branch keywords. + /// + private static readonly HashSet javaBranchKeywords = new() + { + "FOR", "IF", "SWITCH", "TRY", "WHILE" + }; + + /// + /// Set of antlr type names for Java integer and floating point literals. + /// + private static readonly HashSet javaNumbers = new() { "IntegerLiteral", "FloatingPointLiteral" }; + + /// Set of antlr type names for Java character and string literals. + private static readonly HashSet javaStrings = new() { "CharacterLiteral", "StringLiteral" }; + + /// Set of antlr type names for Java separators and operators. + private static readonly HashSet javaPunctuation = new() + { + "LPAREN", "RPAREN", "LBRACE", + "RBRACE", "LBRACK", "RBRACK", "SEMI", "COMMA", "DOT", "ELLIPSIS", "AT", "COLONCOLON", + "ASSIGN", "GT", "LT", "BANG", "TILDE", "QUESTION", "COLON", "ARROW", "EQUAL", "LE", "GE", "NOTEQUAL", "AND", + "OR", "INC", "DEC", "ADD", "SUB", "MUL", "DIV", "BITAND", "BITOR", "CARET", "MOD", + "ADD_ASSIGN", "SUB_ASSIGN", "MUL_ASSIGN", "DIV_ASSIGN", "AND_ASSIGN", "OR_ASSIGN", "XOR_ASSIGN", + "MOD_ASSIGN", "LSHIFT_ASSIGN", "RSHIFT_ASSIGN", "URSHIFT_ASSIGN" + }; + + /// Set of antlr type names for Java identifiers. + private static readonly HashSet javaIdentifiers = new() { "Identifier" }; + + /// + /// Set of antlr type names for Java whitespace. + /// + private static readonly HashSet javaWhitespace = new() { "WS" }; + + /// + /// Set of antlr type names for Java newlines. + /// + private static readonly HashSet javaNewlines = new() { "NEWLINE" }; + + /// + /// Set of antlr type names for Java comments. + /// + private static readonly HashSet javaComments = new() { "COMMENT", "LINE_COMMENT" }; + + #endregion + + #region C# Language + + /// + /// Name of the C# antlr grammar lexer. + /// + private const string cSharpFileName = "CSharpLexer.g4"; + + /// + /// Set of CSharp file extensions. + /// + private static readonly HashSet cSharpExtensions = new() + { + "cs" + }; + + /// + /// Set of antlr type names for CSharp keywords excluding . + /// + private static readonly HashSet cSharpKeywords = new() + { + // General keywords + "ABSTRACT", "ADD", "ALIAS", "ARGLIST", "AS", "ASCENDING", "ASYNC", "AWAIT", "BASE", "BOOL", "BREAK", "BY", + "BYTE", "CASE", "CATCH", "CHAR", "CHECKED", "CLASS", "CONST", "CONTINUE", "DECIMAL", "DEFAULT", "DELEGATE", + "DESCENDING", "DO", "DOUBLE", "DYNAMIC", "ELSE", "ENUM", "EQUALS", "EVENT", "EXPLICIT", "EXTERN", "FALSE", + "FINALLY", "FIXED", "FLOAT", "FROM", "GET", "GOTO", "GROUP", "IMPLICIT", "IN", "INT", + "INTERFACE", "INTERNAL", "INTO", "IS", "JOIN", "LET", "LOCK", "LONG", "NAMEOF", "NAMESPACE", "NEW", "NULL_", + "OBJECT", "ON", "OPERATOR", "ORDERBY", "OUT", "OVERRIDE", "PARAMS", "PARTIAL", "PRIVATE", "PROTECTED", + "PUBLIC", "READONLY", "REF", "REMOVE", "RETURN", "SBYTE", "SEALED", "SELECT", "SET", "SHORT", "SIZEOF", + "STACKALLOC", "STATIC", "STRING", "STRUCT", "THIS", "THROW", "TRUE", "TYPEOF", "UINT", + "ULONG", "UNCHECKED", "UNMANAGED", "UNSAFE", "USHORT", "USING", "VAR", "VIRTUAL", "VOID", "VOLATILE", "WHEN", + "WHERE", "YIELD", "SHARP", + // Directive keywords (anything within a directive is treated as a keyword, similar to IDEs + "DIRECTIVE_TRUE", "DIRECTIVE_FALSE", "DEFINE", "UNDEF", "DIRECTIVE_IF", + "ELIF", "DIRECTIVE_ELSE", "ENDIF", "LINE", "ERROR", "WARNING", "REGION", "ENDREGION", "PRAGMA", "NULLABLE", + "DIRECTIVE_DEFAULT", "DIRECTIVE_HIDDEN", "DIRECTIVE_OPEN_PARENS", "DIRECTIVE_CLOSE_PARENS", "DIRECTIVE_BANG", + "DIRECTIVE_OP_EQ", "DIRECTIVE_OP_NE", "DIRECTIVE_OP_AND", "DIRECTIVE_OP_OR", "CONDITIONAL_SYMBOL", + }; + + /// + /// Set of antlr type names for CSharp branch keywords. + /// + private static readonly HashSet cSharpBranchKeywords = new() + { + "FOR", "FOREACH", "IF", "SWITCH", "TRY", "WHILE" + }; + + /// + /// Set of antlr type names for CSharp integer and floating point literals. + /// + private static readonly HashSet cSharpNumbers = new() + { + "LITERAL_ACCESS", "INTEGER_LITERAL", "HEX_INTEGER_LITERAL", "BIN_INTEGER_LITERAL", "REAL_LITERAL", "DIGITS" + }; + + /// Set of antlr type names for CSharp character and string literals. + private static readonly HashSet cSharpStrings = new() + { + "CHARACTER_LITERAL", "REGULAR_STRING", "VERBATIUM_STRING", "INTERPOLATED_REGULAR_STRING_START", + "INTERPOLATED_VERBATIUM_STRING_START", "VERBATIUM_DOUBLE_QUOTE_INSIDE", + "DOUBLE_QUOTE_INSIDE", "REGULAR_STRING_INSIDE", "VERBATIUM_INSIDE_STRING" + }; + + /// Set of antlr type names for CSharp separators and operators. + private static readonly HashSet cSharpPunctuation = new() + { + "OPEN_BRACE", "CLOSE_BRACE", "CLOSE_BRACE_INSIDE", "OPEN_BRACKET", + "CLOSE_BRACKET", "OPEN_PARENS", "CLOSE_PARENS", "DOT", "COMMA", "FORMAT_STRING", "COLON", "SEMICOLON", "PLUS", "MINUS", "STAR", "DIV", + "PERCENT", "AMP", "BITWISE_OR", "CARET", "BANG", "TILDE", "ASSIGNMENT", "LT", "GT", "INTERR", "DOUBLE_COLON", + "OP_COALESCING", "OP_INC", "OP_DEC", "OP_AND", "OP_OR", "OP_PTR", "OP_EQ", "OP_NE", "OP_LE", "OP_GE", "OP_ADD_ASSIGNMENT", + "OP_SUB_ASSIGNMENT", "OP_MULT_ASSIGNMENT", "OP_DIV_ASSIGNMENT", "OP_MOD_ASSIGNMENT", "OP_AND_ASSIGNMENT", "OP_OR_ASSIGNMENT", + "OP_XOR_ASSIGNMENT", "OP_LEFT_SHIFT", "OP_LEFT_SHIFT_ASSIGNMENT", "OP_COALESCING_ASSIGNMENT", "OP_RANGE", + "DOUBLE_CURLY_INSIDE", "OPEN_BRACE_INSIDE", "REGULAR_CHAR_INSIDE" + }; + + /// Set of antlr type names for CSharp identifiers. + private static readonly HashSet cSharpIdentifiers = new() + { + "IDENTIFIER", "TEXT" + }; + + /// + /// Set of antlr type names for CSharp whitespace. + /// + private static readonly HashSet cSharpWhitespace = new() + { + "WHITESPACES", "DIRECTIVE_WHITESPACES" + }; + + /// + /// Set of antlr type names for CSharp newlines. + /// + private static readonly HashSet cSharpNewlines = new() + { + "NL", "TEXT_NEW_LINE", "DIRECTIVE_NEW_LINE" + }; + + /// + /// Set of antlr type names for Java comments. + /// + private static readonly HashSet cSharpComments = new() + { + "SINGLE_LINE_DOC_COMMENT", "DELIMITED_DOC_COMMENT", "SINGLE_LINE_COMMENT", "DELIMITED_COMMENT", + "DIRECTIVE_SINGLE_LINE_COMMENT" + }; + + #endregion + + #region CPP Language + + /// + /// Name of the antlr grammar lexer. + /// + private const string cppFileName = "CPP14Lexer.g4"; + + /// + /// Set of CPP file extensions. + /// + private static readonly HashSet cppExtensions = new() + { + "cpp", "cxx", "hpp" + }; + + /// + /// Set of antlr type names for CPP keywords excluding . + /// + private static readonly HashSet cppKeywords = new() + { + "Alignas", "Alignof", "Asm", "Auto", "Bool", "Break", "Case", "Catch", "Continue", + "Char", "Char16", "Char32", "Class", "Const", "Constexpr", "Const_cast", + "Decltype", "Default", "Delete", "Do", "Double", "Dynamic_cast", "Else", + "Enum", "Explicit", "Export", "Extern", "False_", "Final", "Float", + "Friend", "Goto", "Inline", "Int", "Long", "Mutable", "Namespace", + "New", "Noexcept", "Nullptr", "Operator", "Override", "Private", "Protected", + "Public", "Register", "Reinterpret_cast", "Return", "Short", "Signed", + "Sizeof", "Static", "Static_assert", "Static_cast", "Struct", + "Template", "This", "Thread_local", "Throw", "True_", "Typedef", + "Typeid_", "Typename_", "Union", "Unsigned", "Using", "Virtual", "Void", + "Volatile", "Wchar", + "BooleanLiteral", "PointerLiteral", "UserDefinedLiteral", + "MultiLineMacro", "Directive" + }; + + /// + /// Set of antlr type names for CPP branch keywords. + /// + private static readonly HashSet cppBranchKeywords = new() + { + "For", "If", "Switch", "Try", "While" + }; + + /// + /// Set of antlr type names for CPP integer and floating point literals. + /// + private static readonly HashSet cppNumbers = new() + { + "IntegerLiteral", "FloatingLiteral", "DecimalLiteral", "OctalLiteral", "HexadecimalLiteral", + "BinaryLiteral", "Integersuffix", "UserDefinedIntegerLiteral", "UserDefinedFloatingLiteral" + }; + + /// Set of antlr type names for CPP character and string literals. + private static readonly HashSet cppStrings = new() + { + "StringLiteral", "CharacterLiteral", "UserDefinedStringLiteral", "UserDefinedCharacterLiteral" + }; + + /// Set of antlr type names for CPP separators and operators. + private static readonly HashSet cppPunctuation = new() + { + "LeftParen", "RightParen", "LeftBracket", + "RightBracket", "LeftBrace", "RightBrace", "Plus", "Minus", "Star", "Div", + "Mod", "Caret", "And", "Or", "Tilde", "Not", "Assign", "Less", "Greater", + "PlusAssign", "MinusAssign", "StarAssign", "DivAssign", "ModAssign", "XorAssign", + "AndAssign", "OrAssign", "LeftShiftAssign", "RightShiftAssign", "Equal", + "NotEqual", "LessEqual", "GreaterEqual", "AndAnd", "OrOr", "PlusPlus", + "MinusMinus", "Comma", "ArrowStar", "Arrow", "Question", "Colon", "Doublecolon", + "Semi", "Dot", "DotStar", "Ellipsis" + }; + + /// Set of antlr type names for CPP identifiers. + private static readonly HashSet cppIdentifiers = new() { "Identifier" }; + + /// + /// Set of antlr type names for CPP whitespace. + /// + private static readonly HashSet cppWhitespace = new() { "Whitespace" }; + + /// + /// Set of antlr type names for CPP newlines. + /// + private static readonly HashSet cppNewlines = new() { "Newline" }; + + /// + /// Set of antlr type names for CPP comments. + /// + private static readonly HashSet cppComments = new() { "BlockComment", "LineComment" }; + + #endregion + + #region Plain Text "Language" + + /// + /// Name of the antlr grammar lexer. + /// + private const string plainFileName = "PlainTextLexer.g4"; + + /// + /// Set of plain text file extensions. + /// Note that this is a special case, since this is the lexer we'll use when nothing else is available. + /// + private static readonly HashSet plainExtensions = new(); + + /// Set of antlr type names for keywords. There are none here. + private static readonly HashSet plainKeywords = new(); + + /// Set of antlr type names for branch keywords. There are none here. + private static readonly HashSet plainBranchKeywords = new(); + + /// Set of antlr type names for numbers. + private static readonly HashSet plainNumbers = new(); + + /// Set of antlr type names for character and string literals. There are none here. + private static readonly HashSet plainStrings = new(); + + /// Set of antlr type names for punctuation. + private static readonly HashSet plainPunctuation = new(); + + /// Set of antlr type names for identifiers, which in this case is for normal words. + private static readonly HashSet plainIdentifiers = new() + { + "WORD", "PSEUDOWORD", "LETTERS", "SIGNS", "SPECIAL", "NUMBERS" + }; + + /// Set of antlr type names for whitespace. + private static readonly HashSet plainWhitespace = new() { "WHITESPACES" }; + + /// Set of antlr type names for newlines. + private static readonly HashSet plainNewlines = new() { "NEWLINES" }; + + /// Set of antlr type names for comments. There are none here. + private static readonly HashSet plainComments = new(); + + #endregion + + #region Static Types + + /// + /// Token Language for Java. + /// + public static readonly AntlrLanguage Java = new(javaFileName, javaExtensions, javaKeywords, javaBranchKeywords, javaNumbers, + javaStrings, javaPunctuation, javaIdentifiers, javaWhitespace, javaNewlines, javaComments); + + /// + /// Token Language for C#. + /// + public static readonly AntlrLanguage CSharp = new(cSharpFileName, cSharpExtensions, cSharpKeywords, cSharpBranchKeywords, cSharpNumbers, + cSharpStrings, cSharpPunctuation, cSharpIdentifiers, cSharpWhitespace, cSharpNewlines, cSharpComments); + + /// + /// Token Language for CPP. + /// + public static readonly AntlrLanguage CPP = new(cppFileName, cppExtensions, cppKeywords, cppBranchKeywords, cppNumbers, + cppStrings, cppPunctuation, cppIdentifiers, cppWhitespace, cppNewlines, cppComments); + + /// + /// Token language for plain text. + /// + public static readonly AntlrLanguage Plain = new(plainFileName, plainExtensions, plainKeywords, plainBranchKeywords, plainNumbers, + plainStrings, plainPunctuation, plainIdentifiers, plainWhitespace, plainNewlines, plainComments); + + #endregion + + public static IEnumerable AllAntlrLanguages => AllTokenLanguages.OfType(); + + /// + /// Constructor for the token language. + /// + /// Should never be accessible from outside this class. + /// Name of this lexer grammar + /// List of file extensions for this language + /// Keywords of this language + /// Number literals of this language + /// String literals of this language + /// Punctuation for this language + /// Identifiers for this language + /// Whitespace for this language + /// Newlines for this language + /// Comments for this language + /// Branches for this language + /// Number of spaces a tab is equivalent to + private AntlrLanguage(string lexerFileName, ISet fileExtensions, ISet keywords, ISet branchKeywords, + ISet numberLiterals, ISet stringLiterals, ISet punctuation, + ISet identifiers, ISet whitespace, ISet newline, + ISet comments, int tabWidth = defaultTabWidth) : base(lexerFileName, fileExtensions, tabWidth) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (AllAntlrLanguages.Except(new []{this}).Any(x => x.LexerFileName == lexerFileName || x.FileExtensions.Overlaps(fileExtensions))) + { + throw new ArgumentException("Lexer file name and file extensions must be unique per language!"); + } + if (AnyOverlaps()) + { + throw new ArgumentException("Symbolic names may not appear in more than one set each!"); + } +#endif + LexerFileName = lexerFileName; + Keywords = keywords; + BranchKeywords = branchKeywords; + NumberLiterals = numberLiterals; + StringLiterals = stringLiterals; + Punctuation = punctuation; + Identifiers = identifiers; + Whitespace = whitespace; + Newline = newline; + Comments = comments; + + return; + + // Check whether any of the symbolic names are used twice + bool AnyOverlaps() + { + return keywords.Intersect(numberLiterals).Intersect(stringLiterals).Intersect(punctuation) + .Intersect(identifiers).Intersect(whitespace).Intersect(newline) + .Intersect(comments).Intersect(branchKeywords).Any(); + } + } + + /// + /// Returns the matching token language for the given . + /// If no matching token language is found, an exception will be thrown. + /// + /// File name of the antlr lexer. Can be found in lexer.GrammarFileName + /// The matching token language + /// If the given is not supported. + public static AntlrLanguage FromLexerFileName(string lexerFileName) + { + return AllAntlrLanguages.SingleOrDefault(x => x.LexerFileName.Equals(lexerFileName)) + ?? throw new ArgumentException($"The given {nameof(lexerFileName)} is not of a supported grammar. Supported grammars are " + + string.Join(", ", AllAntlrLanguages.Select(x => x.LexerFileName))); + } + + /// + /// Returns the matching token language for the given . + /// If no matching token language is found, the will be used, unless + /// is true. + /// + /// File extension for the language. + /// + /// Whether to throw an exception when an unknown file extension is encountered. + /// If this is false, the will be used instead in such a case. + /// + /// The matching token language. + /// + /// If the given is not supported and is true. + /// + public static AntlrLanguage FromFileExtension(string extension, bool throwOnUnknown = false) + { + AntlrLanguage target = AllAntlrLanguages.SingleOrDefault(x => x.FileExtensions.Contains(extension)); + if (target == null) + { + if (throwOnUnknown) + { + throw new ArgumentException("The given filetype is not supported in Antlr. Supported filetypes are " + + string.Join(", ", AllAntlrLanguages.SelectMany(x => x.FileExtensions))); + } + + target = Plain; + } + + return target; + } + + + /// + /// Creates a new lexer matching the of this language. + /// + /// The string which shall be parsed by the lexer. + /// the new matching lexer + /// If no lexer is defined for this language. + public Lexer CreateLexer(string content) + { + ICharStream input = CharStreams.fromString(content); + return LexerFileName switch + { + javaFileName => new Java9Lexer(input), + cSharpFileName => new CSharpLexer(input), + cppFileName => new CPP14Lexer(input), + plainFileName => new PlainTextLexer(input), + _ => throw new InvalidOperationException("No lexer defined for this language yet.") + }; + } + + /// + /// Returns the type of token this is. + /// The type of token will be represented by the name of the collection it is in. + /// Returns null if the token is not any known type. + /// + /// a symbolic name from the antlr lexer for this language + /// name of the type the given is, or null if it isn't known. + public string TypeName(string token) + { + // We go through each category and check whether it contains the token. + // I know that this looks like it may be abstracted because the same thing is done on different objects + // in succession, but due to the usage of nameof() a refactoring of this kind would break it. + if (Keywords.Contains(token)) + { + return nameof(Keywords); + } + if (BranchKeywords.Contains(token)) + { + return nameof(BranchKeywords); + } + if (NumberLiterals.Contains(token)) + { + return nameof(NumberLiterals); + } + if (StringLiterals.Contains(token)) + { + return nameof(StringLiterals); + } + if (Punctuation.Contains(token)) + { + return nameof(Punctuation); + } + if (Identifiers.Contains(token)) + { + return nameof(Identifiers); + } + if (Comments.Contains(token)) + { + return nameof(Comments); + } + if (Whitespace.Contains(token)) + { + return nameof(Whitespace); + } + if (Newline.Contains(token)) + { + return nameof(Newline); + } + return eof.Equals(token) ? nameof(eof) : null; + } + } +} diff --git a/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs.meta b/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs.meta new file mode 100644 index 0000000000..544a746015 --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrLanguage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3ae2a8a900efeb0faaf5430fff524a7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/Scanner/Antlr/AntlrToken.cs b/Assets/SEE/Scanner/Antlr/AntlrToken.cs new file mode 100644 index 0000000000..e87f068efb --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrToken.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Antlr4.Runtime; +using Cysharp.Threading.Tasks; + +namespace SEE.Scanner.Antlr +{ + /// + /// Represents a token from a source code file, including , a , + /// and some , emitted by an Antlr lexer. + /// + public record AntlrToken : SEEToken + { + private AntlrToken(string Text, TokenType TokenType, AntlrLanguage Language) : base(Text, TokenType, Language) { } + + /// + /// Creates a new from the given scanned by the given + /// Antlr . + /// + /// The token which shall be converted to an + /// The Antlr lexer with which the token was created. + /// The language of the . + /// If this is not given, the language will be inferred from the given 's grammar. + /// The corresponding to the given . + private static AntlrToken FromAntlrIToken(IToken token, Lexer lexer, AntlrLanguage language = null) + { + return new AntlrToken(token.Text, + AntlrTokenType.FromAntlrType(language, lexer.Vocabulary.GetSymbolicName(token.Type)), + language ?? AntlrLanguage.FromLexerFileName(lexer.GrammarFileName)); + } + + /// + /// Returns a stream of s created by parsing the file at the supplied + /// . + /// + /// Path to the source code file which shall be read and parsed. + /// A list of tokens created from the source code file. + /// + ///
    + ///
  • The language of the file will be determined by checking its file extension.
  • + ///
  • Each token will be created by using .
  • + ///
+ ///
+ public static async UniTask> FromFileAsync(string filePath) + { + AntlrLanguage language = AntlrLanguage.FromFileExtension(Path.GetExtension(filePath)?[1..]); + Lexer lexer = language.CreateLexer(await File.ReadAllTextAsync(filePath)); + CommonTokenStream tokenStream = new(lexer); + tokenStream.Fill(); + // Generate list of SEETokens using the token stream and its language + return tokenStream.GetTokens().Select(x => FromAntlrIToken(x, lexer, language)); + } + + /// + /// Returns a list of s created by parsing the given , assuming + /// it's in the given . + /// + /// Text from which the token stream shall be created. + /// Language the given is written in + /// A list of tokens created from the source code file. + public static IList FromString(string text, AntlrLanguage language) + { + Lexer lexer = language.CreateLexer(text); + CommonTokenStream tokenStream = new(lexer); + tokenStream.Fill(); + // Generate list of SEETokens using the token stream and its language + return tokenStream.GetTokens().Select(x => FromAntlrIToken(x, lexer, language)).ToList(); + } + } +} diff --git a/Assets/SEE/Scanner/Antlr/AntlrToken.cs.meta b/Assets/SEE/Scanner/Antlr/AntlrToken.cs.meta new file mode 100644 index 0000000000..4b48cd099a --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrToken.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9af09c72f549db7cae8049266ddd5fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs b/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs new file mode 100644 index 0000000000..e1a136786c --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; + +namespace SEE.Scanner.Antlr +{ + /// + /// Represents a kind of token in an Antlr-supported programming language, with an associated color. + /// For example, this may be a or an . + /// + public record AntlrTokenType : TokenType + { + /// + /// Returns the corresponding for the given + /// in the given . If it's not recognized, an exception is thrown. + /// + /// The language the is from + /// Symbolic name from an antlr lexer + /// The corresponding token for the given . + /// If or is null + /// If the is not recognized + public static TokenType FromAntlrType(AntlrLanguage language, string symbolicName) + { + if (language == null || symbolicName == null) + { + throw new ArgumentNullException(); + } + + string typeName = language.TypeName(symbolicName); + TokenType type = AllTokens.SingleOrDefault(x => x.Name.Equals(typeName)); + if (type == null) + { + throw new InvalidOperationException($"Unknown token type: {typeName}/{symbolicName}"); + } + return type; + } + + #region Static TokenTypes + + // IMPORTANT: The name has to match with the name of the collection in AntlrLanguage! + + /// + /// Keyword tokens. This also includes boolean literals and null literals. + /// + public static readonly AntlrTokenType Keyword = new("Keywords", "#D988F2"); // purple + + /// + /// Branch keyword tokens. + /// + /// We want s have the same color as + /// other s. + public static readonly AntlrTokenType BranchKeyword = new("BranchKeywords", "#D988F2"); // purple + + /// + /// Number literal tokens. This includes integer literals, floating point literals, etc. + /// + public static readonly AntlrTokenType NumberLiteral = new("NumberLiterals", "#D48F35"); // orange + + /// + /// String literal tokens. This also includes character literals. + /// + public static readonly AntlrTokenType StringLiteral = new("StringLiterals", "#92F288"); // light green + + /// + /// Punctuation tokens, such as separators and operators. + /// + public static readonly AntlrTokenType Punctuation = new("Punctuation", "#96E5FF"); // light blue + + /// + /// Identifier tokens, such as variable names. + /// + public static readonly AntlrTokenType Identifier = new("Identifiers", "#FFFFFF"); // white + + /// + /// Comments of any kind. + /// + public static readonly AntlrTokenType Comment = new("Comments", "#6F708E"); // dark bluish gray + + #endregion + + private AntlrTokenType(string name, string color) : base(name, color) + { + } + } +} diff --git a/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs.meta b/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs.meta new file mode 100644 index 0000000000..0c5bec7bb2 --- /dev/null +++ b/Assets/SEE/Scanner/Antlr/AntlrTokenType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c8d7797f2fae4e57a275b98721c9df08 +timeCreated: 1719432972 \ No newline at end of file diff --git a/Assets/SEE/Scanner/LSP.meta b/Assets/SEE/Scanner/LSP.meta new file mode 100644 index 0000000000..8dadd5cb51 --- /dev/null +++ b/Assets/SEE/Scanner/LSP.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ed9affb576424a6b95cb933ff3ea0182 +timeCreated: 1719516199 \ No newline at end of file diff --git a/Assets/SEE/Scanner/LSP/LSPToken.cs b/Assets/SEE/Scanner/LSP/LSPToken.cs new file mode 100644 index 0000000000..a7aaae88df --- /dev/null +++ b/Assets/SEE/Scanner/LSP/LSPToken.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Cysharp.Threading.Tasks; +using MoreLinq; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using SEE.Tools.LSP; +using UnityEngine.Assertions; + +namespace SEE.Scanner.LSP +{ + /// + /// Represents a semantic token from a source code file, including , a , + /// and some , emitted by a language server. + /// + public record LSPToken : SEEToken + { + private LSPToken(string Text, TokenType TokenType, TokenModifiers Modifiers, LSPLanguage Language) : base(Text, TokenType, Language, Modifiers) { } + + /// + /// Returns a stream of s created by parsing the file at the supplied + /// . The tokens are generated for the given , + /// using the given LSP . + /// + /// Path to the source code file which shall be read and parsed. + /// The LSP handler used to retrieve the semantic tokens. + /// The language of the file. + /// A stream of tokens created from the source code file. + public static async UniTask> FromFileAsync(string filePath, LSPHandler handler, LSPLanguage language) + { + const int delayMs = 100; + const int maxTries = 30; + + string fileContents = await System.IO.File.ReadAllTextAsync(filePath); + // We may need to wait for the server to process the file, so we just retry for a bit + // (with the above constants, at most 3 seconds) until we get the tokens. + SemanticTokens tokens; + int tries = 0; + do + { + tokens = await handler.GetSemanticTokensAsync(filePath); + if (tokens.Data.Length == 0) + { + await UniTask.Delay(delayMs); + } + } while (tokens.Data.Length == 0 && ++tries < maxTries); + if (handler.ServerCapabilities.SemanticTokensProvider == null) + { + throw new InvalidOperationException("The server does not support semantic tokens."); + } + return FromSemanticTokens(tokens, handler.ServerCapabilities.SemanticTokensProvider.Legend, language, fileContents); + } + + /// + /// Returns a stream of s from the given LSP-generated + /// and , for the given and . + /// + /// The semantic tokens to be converted to s. + /// The legend used to interpret the semantic tokens. + /// The language of the file. + /// The contents of the file the tokens are from as a single string. + /// A stream of tokens created from the semantic tokens. + private static IEnumerable FromSemanticTokens(SemanticTokens tokens, SemanticTokensLegend legend, LSPLanguage language, string fileContents) + { + IList modifierLegend = legend.TokenModifiers.ToList(); + IList typeLegend = legend.TokenTypes.ToList(); + + Assert.IsTrue(tokens.Data.Length % 5 == 0, "Semantic tokens data length must be a multiple of 5."); + + // Both cursors are indices within the fileContents. + // tokenCursor is the cursor for the end of the current LSPToken. + int tokenCursor = 0; + // semanticTokenCursor is the cursor for the start of the current semantic token. + int semanticTokenCursor = 0; + for (int i = 0; i < tokens.Data.Length; i += 5) + { + // For a description of the encoding, see the section on semantic tokens in the LSP specification. + + // Token line number, relative to the previous token. + int deltaLine = tokens.Data[i]; + // Token start character, relative to either 0 or the previous token’s start if they are on the same line + int deltaStart = tokens.Data[i + 1]; + // Length of the token + int length = tokens.Data[i + 2]; + // The type of the token, represented as an index within the typeLegend. + int type = tokens.Data[i + 3]; + // The modifiers of the token, represented as a bitmask of indices within the modifierLegend. + int modifiers = tokens.Data[i + 4]; + + if (deltaLine > 0) + { + // We need to skip ahead until the cursor is at the start of the relevant line + // (depending on deltaLine). + for (int j = 0; j < deltaLine; j++) + { + semanticTokenCursor = fileContents.IndexOf('\n', semanticTokenCursor) + 1; + } + } + + // Then, we need to move the cursor ahead by deltaStart. + semanticTokenCursor += deltaStart; + + // The tokens given by LSP may not encompass the whole document. + // For example, there are no LSP token types for newlines and whitespace. + // Hence, we have to fill these gaps ourselves by constructing tokens for them manually. + if (semanticTokenCursor > tokenCursor) + { + // We now have to differentiate between Newlines, Whitespace, and other tokens. + string gap = fileContents.Substring(tokenCursor, semanticTokenCursor - tokenCursor); + foreach (LSPToken token in HandleGap(gap)) + { + yield return token; + } + } + + tokenCursor = semanticTokenCursor + length; + + string tokenText = fileContents[semanticTokenCursor..tokenCursor]; + LSPTokenType tokenType = LSPTokenType.FromSemanticTokenType(typeLegend[type]); + // Modifiers are a bitmask, so we need to map them to the actual modifiers using the legend. + TokenModifiers tokenModifiers = modifierLegend.Where((_, index) => (modifiers & (1 << index)) != 0) + .Aggregate(TokenModifiers.None, (x, y) => x | y.FromLspTokenModifier()); + yield return new LSPToken(tokenText, tokenType, tokenModifiers, language); + } + + // There may be leftover tokens at the end of the file. + if (tokenCursor < fileContents.Length) + { + string gap = fileContents[tokenCursor..]; + foreach (LSPToken token in HandleGap(gap)) + { + yield return token; + } + } + + yield return new LSPToken(string.Empty, TokenType.EOF, TokenModifiers.None, language); + yield break; + + IEnumerable HandleGap(string gap) + { + // This gap may consist of multiple tokens. + // For example, the string ";\n //" consists of four tokens: + // A semicolon, a newline, four spaces, and two slashes. + return Regex.Split(gap, @"(?<=\S)(?=\s)|(?<=\s)(?=\S)").SelectMany(HandleGapToken); + } + + IEnumerable HandleGapToken(string gapToken) + { + if (string.IsNullOrWhiteSpace(gapToken)) + { + Assert.IsNotNull(gapToken); + // This can still contain newlines, which we need to handle separately. + string[] gapTokens = gapToken.Split('\n'); + foreach (string whitespace in gapTokens.Interleave(MoreEnumerable.Return("\n").Repeat(gapTokens.Length - 1))) + { + if (whitespace == "\n") + { + yield return new LSPToken(whitespace, TokenType.Newline, TokenModifiers.None, language); + } + else + { + yield return new LSPToken(whitespace, TokenType.Whitespace, TokenModifiers.None, language); + } + } + } + else + { + yield return new LSPToken(gapToken, LSPTokenType.Type, TokenModifiers.None, language); + } + } + } + } +} diff --git a/Assets/SEE/Scanner/LSP/LSPToken.cs.meta b/Assets/SEE/Scanner/LSP/LSPToken.cs.meta new file mode 100644 index 0000000000..695ac94b96 --- /dev/null +++ b/Assets/SEE/Scanner/LSP/LSPToken.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b1fdc2c975e8456782f48b3d0090dbbd +timeCreated: 1719516220 \ No newline at end of file diff --git a/Assets/SEE/Scanner/LSP/LSPTokenType.cs b/Assets/SEE/Scanner/LSP/LSPTokenType.cs new file mode 100644 index 0000000000..466dea678a --- /dev/null +++ b/Assets/SEE/Scanner/LSP/LSPTokenType.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace SEE.Scanner.LSP +{ + /// + /// Represents a kind of token in an LSP-supported programming language, with an associated color. + /// For example, this may be a or an . + /// + public record LSPTokenType : TokenType + { + private LSPTokenType(string name, string color) : base(name, color) { } + + /// + /// For identifiers that declare or reference a namespace, module, or package. + /// + public static readonly LSPTokenType Namespace = new("namespace", "#FFCB6B"); + + /// + /// For identifiers that declare or reference a class type. + /// + public static readonly LSPTokenType Class = new("class", "#FFCB6B"); + + /// + /// For identifiers that declare or reference an enumeration type. + /// + public static readonly LSPTokenType Enum = new("enum", "#F78C6C"); + + /// + /// For identifiers that declare or reference an interface type. + /// + public static readonly LSPTokenType Interface = new("interface", "#C3E88D"); + + /// + /// For identifiers that declare or reference a struct type. + /// + public static readonly LSPTokenType Struct = new("struct", "#FFCB6B"); + + /// + /// For identifiers that declare or reference a type parameter. + /// + public static readonly LSPTokenType TypeParameter = new("typeParameter", "#C3E88D"); + + /// + /// For identifiers that declare or reference function or method parameter. + /// + public static readonly LSPTokenType Parameter = new("parameter", "#F78C6C"); + /// + /// For identifiers that declare or reference a local or global variable. + /// + public static readonly LSPTokenType Variable = new("variable", "#EEFFE3"); + /// + /// For identifiers that declare or reference a member property, member field, or member variable. + /// + public static readonly LSPTokenType Property = new("property", "#EEFFFF"); + + /// + /// For identifiers that declare or reference an enumeration property, constant, or member. + /// + public static readonly LSPTokenType EnumMember = new("enumMember", "#F78C6C"); + + /// + /// For identifiers that declare an event property. + /// + public static readonly LSPTokenType Event = new("event", "#EEFFE3"); + + /// + /// For identifiers that declare a function. + /// + public static readonly LSPTokenType Function = new("function", "#82AAFF"); + + /// + /// For identifiers that declare a member function or method. + /// + public static readonly LSPTokenType Method = new("method", "#82AAFF"); + + /// + /// For identifiers that declare a macro. + /// + public static readonly LSPTokenType Macro = new("macro", "#C792EA"); + + /// + /// For tokens that represent a language keyword. + /// + public static readonly LSPTokenType Keyword = new("keyword", "#C792EA"); + + /// + /// For tokens that represent a modifier. + /// + public static readonly LSPTokenType Modifier = new("modifier", "#C792EA"); + + /// + /// For tokens that represent a comment. + /// + public static readonly LSPTokenType Comment = new("comment", "#717CB4"); + + /// + /// For tokens that represent a string literal. + /// + public static readonly LSPTokenType String = new("string", "#C3E88D"); + + /// + /// For tokens that represent a number literal. + /// + public static readonly LSPTokenType Number = new("number", "#F78C6C"); + + /// + /// For tokens that represent a regular expression literal. + /// + public static readonly LSPTokenType Regexp = new("regexp", "#93E88D"); + + /// + /// For tokens that represent an operator. + /// + public static readonly LSPTokenType Operator = new("operator", "#89DDFF"); + + /// + /// For identifiers that declare or reference decorators and annotations. + /// + public static readonly LSPTokenType Decorator = new("decorator", "#FFCB6B"); + + /// + /// For identifiers that declare a label. + /// + public static readonly LSPTokenType Label = new("label", "#C3D3DE"); + + /// + /// Represents a generic type. Acts as a fallback for types which can't be mapped to one of the other types. + /// + public static readonly LSPTokenType Type = new("type", "#FFFFFF"); + + /// + /// A mapping of to . + /// + private static readonly Dictionary semanticTokenMapping = new() + { + { SemanticTokenType.Comment, Comment }, + { SemanticTokenType.Keyword, Keyword }, + { SemanticTokenType.String, String }, + { SemanticTokenType.Number, Number }, + { SemanticTokenType.Regexp, Regexp }, + { SemanticTokenType.Operator, Operator }, + { SemanticTokenType.Namespace, Namespace }, + { SemanticTokenType.Type, Type }, + { SemanticTokenType.Struct, Struct }, + { SemanticTokenType.Class, Class }, + { SemanticTokenType.Interface, Interface }, + { SemanticTokenType.Enum, Enum }, + { SemanticTokenType.TypeParameter, TypeParameter }, + { SemanticTokenType.Function, Function }, + { SemanticTokenType.Method, Method }, + { SemanticTokenType.Property, Property }, + { SemanticTokenType.Macro, Macro }, + { SemanticTokenType.Variable, Variable }, + { SemanticTokenType.Parameter, Parameter }, + { SemanticTokenType.Label, Label }, + { SemanticTokenType.Modifier, Modifier }, + { SemanticTokenType.Event, Event }, + { SemanticTokenType.EnumMember, EnumMember }, + { SemanticTokenType.Decorator, Decorator } + }; + + /// + /// Returns the that corresponds to the given . + /// + /// The to map to an . + /// The that corresponds to the given . + public static LSPTokenType FromSemanticTokenType(SemanticTokenType semanticTokenType) => semanticTokenMapping.GetValueOrDefault(semanticTokenType, Type); + } +} diff --git a/Assets/SEE/Scanner/LSP/LSPTokenType.cs.meta b/Assets/SEE/Scanner/LSP/LSPTokenType.cs.meta new file mode 100644 index 0000000000..6f6eff39a1 --- /dev/null +++ b/Assets/SEE/Scanner/LSP/LSPTokenType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a07bb9a9e8e24b58bf95f2ea9a7cec74 +timeCreated: 1719516212 \ No newline at end of file diff --git a/Assets/SEE/Scanner/SEEToken.cs b/Assets/SEE/Scanner/SEEToken.cs index c7a035dca7..bf52a934ef 100644 --- a/Assets/SEE/Scanner/SEEToken.cs +++ b/Assets/SEE/Scanner/SEEToken.cs @@ -1,257 +1,12 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Antlr4.Runtime; -using UnityEngine; - -namespace SEE.Scanner +namespace SEE.Scanner { /// - /// Represents a token from a source code file, including and a . + /// Represents a token from a source code file, including , a , + /// and some . /// - public class SEEToken - { - /// - /// The text of the token. - /// - public readonly string Text; - - /// - /// The type of this token. - /// - public readonly Type TokenType; - - /// - /// The inclusive index of the beginning of this token. - /// The index is measured from the beginning of the file, whereas the first character has the index 0. - /// - public readonly int StartOffset; - - /// - /// The exclusive index of the end of this token. - /// In other words, this is the index of the first character not belonging to this token. - /// The index is measured from the beginning of the file, whereas the first character has the index 0. - /// - public readonly int EndOffset; - - /// - /// The language of the source code this token was parsed from. - /// - public readonly TokenLanguage Language; - - /// - /// Constructor for this class. - /// - /// Text of this token. Must not be null. - /// Type of this token. Must not be null. - /// Start offset of this token. Must not be negative, except for the special value - /// -1, which indicates the very first newline of the file - /// End offset of this token. Must not be smaller than . - /// The language of the source code this token is from - /// - /// If or is null. - /// - /// - /// If is smaller than - /// - /// If is less than -1. - public SEEToken(string text, Type type, int startOffset, int endOffset, TokenLanguage language) - { - Text = text ?? throw new ArgumentNullException(nameof(text)); - TokenType = type ?? throw new ArgumentNullException(nameof(type)); - if (endOffset < startOffset) - { - throw new ArgumentOutOfRangeException($"{nameof(endOffset)} must not be smaller than {nameof(startOffset)}!"); - } - // endOffset is greater than startOffset at this point, so it can't be negative after this part - else if (startOffset < -1) - { - throw new ArgumentException($"{nameof(startOffset)} must not be less than -1!"); - } - - StartOffset = startOffset; - EndOffset = endOffset; - Language = language; - } - - /// - /// Creates a new from the given parsed by the given - /// . - /// - /// The token which shall be converted to a - /// The lexer with which the token was created. - /// The language of the . - /// If this is not given, the language will be inferred from the given 's grammar. - /// The corresponding to the given . - public static SEEToken FromAntlrToken(IToken token, Lexer lexer, TokenLanguage language = null) - { - language ??= TokenLanguage.FromLexerFileName(lexer.GrammarFileName); - return new SEEToken(token.Text, - Type.FromAntlrType(language, lexer.Vocabulary.GetSymbolicName(token.Type)), - token.StartIndex, token.StopIndex + 1, language); // Antlr StopIndex is inclusive - } - - /// - /// Returns a list of s created by parsing the file at the supplied - /// . - /// - /// Path to the source code file which shall be read and parsed. - /// A list of tokens created from the source code file. - /// - ///
    - ///
  • The language of the file will be determined by checking its file extension.
  • - ///
  • Each token will be created by using .
  • - ///
- ///
- public static IEnumerable FromFile(string filename) - { - TokenLanguage language = TokenLanguage.FromFileExtension(Path.GetExtension(filename)?[1..]); - Lexer lexer = language.CreateLexer(File.ReadAllText(filename)); - CommonTokenStream tokenStream = new(lexer); - tokenStream.Fill(); - // Generate list of SEETokens using the token stream and its language - return tokenStream.GetTokens().Select(x => FromAntlrToken(x, lexer, language)); - } - - /// - /// Returns a list of s created by parsing the given , assuming - /// it's in the given . - /// - /// Text from which the token stream shall be created. - /// Language the given is written in - /// A list of tokens created from the source code file. - public static IList FromString(string text, TokenLanguage language) - { - Lexer lexer = language.CreateLexer(text); - CommonTokenStream tokenStream = new(lexer); - tokenStream.Fill(); - // Generate list of SEETokens using the token stream and its language - return tokenStream.GetTokens().Select(x => FromAntlrToken(x, lexer, language)).ToList(); - } - - /// - /// Represents a kind of token in a programming language, with an associated color. - /// For example, this may be a or an . - /// - public class Type - { - /// - /// Name of the token type. - /// This has to match with the corresponding collection in . - /// - public string Name { get; } - - /// - /// Color the token type should be rendered in in hexadecimal RGB notation (no '#' sign). - /// An optional fourth byte may be entered to define the alpha value. - /// - /// Red would be "FF0000". Semitransparent black would be "00000088". - public string Color { get; } - - #region Static Types - - /// - /// A list of all possible tokens. - /// - public static IList AllTokens { get; } = new List(); - - // IMPORTANT: The name has to match with the name of the collection in TokenLanguage! - - /// - /// Keyword tokens. This also includes boolean literals and null literals. - /// - public static readonly Type Keyword = new("Keywords", "D988F2"); // purple - - /// - /// Branch keyword tokens. - /// - /// We want s have the same color as - /// other s. - public static readonly Type BranchKeyword = new("BranchKeywords", "D988F2"); // purple - - /// - /// Number literal tokens. This includes integer literals, floating point literals, etc. - /// - public static readonly Type NumberLiteral = new("NumberLiterals", "D48F35"); // orange - - /// - /// String literal tokens. This also includes character literals. - /// - public static readonly Type StringLiteral = new("StringLiterals", "92F288"); // light green - - /// - /// Punctuation tokens, such as separators and operators. - /// - public static readonly Type Punctuation = new("Punctuation", "96E5FF"); // light blue - - /// - /// Identifier tokens, such as variable names. - /// - public static readonly Type Identifier = new("Identifiers", "FFFFFF"); // white - - /// - /// Comments of any kind. - /// - public static readonly Type Comment = new("Comments", "6F708E"); // dark bluish gray - - /// - /// Whitespace tokens, excluding newlines. - /// - public static readonly Type Whitespace = new("Whitespace", "000000"); // color doesn't matter - - /// - /// Newline tokens. Must contain exactly one newline. - /// - public static readonly Type Newline = new("Newline", "000000"); // color doesn't matter - - /// - /// End-Of-File token. - /// - public static readonly Type EOF = new("eof", "000000"); // color doesn't matter - - /// - /// Unknown tokens, i.e. those not recognized by the lexer. - /// - public static readonly Type Unknown = new("Unknown", "FFFFFF"); // white - - #endregion - - /// - /// Constructor for this class. - /// - /// Must never be accessible from the outside. - /// Name of this token type - /// Color this token type should be shown in - private Type(string name, string color) - { - Color = color; - Name = name; - AllTokens.Add(this); - } - - /// - /// Returns the corresponding for the given in the given - /// . If it's not recognized, will be returned. - /// - /// The language the is from - /// Symbolic name from an antlr lexer - /// The corresponding token for the given . - public static Type FromAntlrType(TokenLanguage language, string symbolicName) - { - if (language == null || symbolicName == null) - { - throw new ArgumentNullException(); - } - - string typeName = language.TypeName(symbolicName); - Type type = AllTokens.SingleOrDefault(x => x.Name.Equals(typeName)); - if (type == null) - { - Debug.LogError($"Unknown token type: {typeName}/{symbolicName}"); - } - return type ?? Unknown; - } - } - } + /// The text of the token. + /// The type of the token (e.g., class). + /// The language of the token. + /// The modifiers of the token (e.g., static. + public abstract record SEEToken(string Text, TokenType TokenType, TokenLanguage Language, TokenModifiers Modifiers = TokenModifiers.None); } diff --git a/Assets/SEE/Scanner/TokenLanguage.cs b/Assets/SEE/Scanner/TokenLanguage.cs index 82be4b2cdd..d9a963b5a9 100644 --- a/Assets/SEE/Scanner/TokenLanguage.cs +++ b/Assets/SEE/Scanner/TokenLanguage.cs @@ -1,36 +1,26 @@ -using System; using System.Collections.Generic; -using System.Linq; -using Antlr4.Runtime; namespace SEE.Scanner { /// - /// Represents a language a is in. - /// Symbolic names for the antlr lexer are specified here. + /// A programming language a is in. /// - public class TokenLanguage + public abstract class TokenLanguage { /// /// Default number of spaces a tab is equivalent to. /// - private const int defaultTabWidth = 4; + protected const int defaultTabWidth = 4; /// /// Language-independent symbolic name for the end of file token. /// - private const string eof = "EOF"; + protected const string eof = "EOF"; /// - /// File extensions which apply for the given language. - /// May not intersect any other languages file extensions. + /// The name of the language. /// - public ISet FileExtensions { get; } - - /// - /// Name of the antlr lexer file the keywords were taken from. - /// - public string LexerFileName { get; } + public string Name { get; } /// /// Number of spaces equivalent to a tab in this language. @@ -39,570 +29,22 @@ public class TokenLanguage public int TabWidth { get; } /// - /// Symbolic names for comments of a language, including block, line, and documentation comments. - /// - public ISet Comments { get; } - - /// - /// Symbolic names for keywords of a language. This also includes boolean literals and null literals. - /// - public ISet Keywords { get; } - - /// - /// Symbolic names for branch keywords of a language. - /// - public ISet BranchKeywords { get; } - - /// - /// Symbolic names for number literals of a language. This includes integer literals, floating point literals, etc. - /// - public ISet NumberLiterals { get; } - - /// - /// Symbolic names for string literals of a language. Also includes character literals. - /// - public ISet StringLiterals { get; } - - /// - /// Symbolic names for separators and operators of a language. - /// - public ISet Punctuation { get; } - - /// - /// Symbolic names for identifiers in a language. - /// - public ISet Identifiers { get; } - - /// - /// Symbolic names for whitespace in a language, excluding newlines. - /// - public ISet Whitespace { get; } - - /// - /// Symbolic names for newlines in a language. - /// - public ISet Newline { get; } - - #region Java Language - - /// - /// Name of the Java antlr grammar lexer. - /// - private const string javaFileName = "Java9Lexer.g4"; - - /// - /// Set of java file extensions. - /// - private static readonly HashSet javaExtensions = new() - { - "java" - }; - - /// - /// Set of antlr type names for Java keywords excluding . - /// - private static readonly HashSet javaKeywords = new() - { - "ABSTRACT", "ASSERT", "BOOLEAN", "BREAK", "BYTE", "CASE", "CATCH", "CHAR", "CLASS", "CONST", "CONTINUE", - "DEFAULT", "DO", "DOUBLE", "ELSE", "ENUM", "EXPORTS", "EXTENDS", "FINAL", "FINALLY", "FLOAT", - "GOTO", "IMPLEMENTS", "IMPORT", "INSTANCEOF", "INT", "INTERFACE", "LONG", "MODULE", "NATIVE", "NEW", - "OPEN", "OPERNS", "PACKAGE", "PRIVATE", "PROTECTED", "PROVIDES", "PUBLIC", "REQUIRES", "RETURN", "SHORT", - "STATIC", "STRICTFP", "SUPER", "SYNCHRONIZED", "THIS", "THROW", "THROWS", "TO", "TRANSIENT", - "TRANSITIVE", "USES", "VOID", "VOLATILE", "WITH", "UNDER_SCORE", - "BooleanLiteral", "NullLiteral" - }; - - /// - /// Set of antlr type names for Java branch keywords. - /// - private static readonly HashSet javaBranchKeywords = new() - { - "FOR", "IF", "SWITCH", "TRY", "WHILE" - }; - - /// - /// Set of antlr type names for Java integer and floating point literals. - /// - private static readonly HashSet javaNumbers = new() { "IntegerLiteral", "FloatingPointLiteral" }; - - /// Set of antlr type names for Java character and string literals. - private static readonly HashSet javaStrings = new() { "CharacterLiteral", "StringLiteral" }; - - /// Set of antlr type names for Java separators and operators. - private static readonly HashSet javaPunctuation = new() - { - "LPAREN", "RPAREN", "LBRACE", - "RBRACE", "LBRACK", "RBRACK", "SEMI", "COMMA", "DOT", "ELLIPSIS", "AT", "COLONCOLON", - "ASSIGN", "GT", "LT", "BANG", "TILDE", "QUESTION", "COLON", "ARROW", "EQUAL", "LE", "GE", "NOTEQUAL", "AND", - "OR", "INC", "DEC", "ADD", "SUB", "MUL", "DIV", "BITAND", "BITOR", "CARET", "MOD", - "ADD_ASSIGN", "SUB_ASSIGN", "MUL_ASSIGN", "DIV_ASSIGN", "AND_ASSIGN", "OR_ASSIGN", "XOR_ASSIGN", - "MOD_ASSIGN", "LSHIFT_ASSIGN", "RSHIFT_ASSIGN", "URSHIFT_ASSIGN" - }; - - /// Set of antlr type names for Java identifiers. - private static readonly HashSet javaIdentifiers = new() { "Identifier" }; - - /// - /// Set of antlr type names for Java whitespace. - /// - private static readonly HashSet javaWhitespace = new() { "WS" }; - - /// - /// Set of antlr type names for Java newlines. - /// - private static readonly HashSet javaNewlines = new() { "NEWLINE" }; - - /// - /// Set of antlr type names for Java comments. - /// - private static readonly HashSet javaComments = new() { "COMMENT", "LINE_COMMENT" }; - - #endregion - - #region C# Language - - /// - /// Name of the C# antlr grammar lexer. - /// - private const string cSharpFileName = "CSharpLexer.g4"; - - /// - /// Set of CSharp file extensions. - /// - private static readonly HashSet cSharpExtensions = new() - { - "cs" - }; - - /// - /// Set of antlr type names for CSharp keywords excluding . - /// - private static readonly HashSet cSharpKeywords = new() - { - // General keywords - "ABSTRACT", "ADD", "ALIAS", "ARGLIST", "AS", "ASCENDING", "ASYNC", "AWAIT", "BASE", "BOOL", "BREAK", "BY", - "BYTE", "CASE", "CATCH", "CHAR", "CHECKED", "CLASS", "CONST", "CONTINUE", "DECIMAL", "DEFAULT", "DELEGATE", - "DESCENDING", "DO", "DOUBLE", "DYNAMIC", "ELSE", "ENUM", "EQUALS", "EVENT", "EXPLICIT", "EXTERN", "FALSE", - "FINALLY", "FIXED", "FLOAT", "FROM", "GET", "GOTO", "GROUP", "IMPLICIT", "IN", "INT", - "INTERFACE", "INTERNAL", "INTO", "IS", "JOIN", "LET", "LOCK", "LONG", "NAMEOF", "NAMESPACE", "NEW", "NULL_", - "OBJECT", "ON", "OPERATOR", "ORDERBY", "OUT", "OVERRIDE", "PARAMS", "PARTIAL", "PRIVATE", "PROTECTED", - "PUBLIC", "READONLY", "REF", "REMOVE", "RETURN", "SBYTE", "SEALED", "SELECT", "SET", "SHORT", "SIZEOF", - "STACKALLOC", "STATIC", "STRING", "STRUCT", "THIS", "THROW", "TRUE", "TYPEOF", "UINT", - "ULONG", "UNCHECKED", "UNMANAGED", "UNSAFE", "USHORT", "USING", "VAR", "VIRTUAL", "VOID", "VOLATILE", "WHEN", - "WHERE", "YIELD", "SHARP", - // Directive keywords (anything within a directive is treated as a keyword, similar to IDEs - "DIRECTIVE_TRUE", "DIRECTIVE_FALSE", "DEFINE", "UNDEF", "DIRECTIVE_IF", - "ELIF", "DIRECTIVE_ELSE", "ENDIF", "LINE", "ERROR", "WARNING", "REGION", "ENDREGION", "PRAGMA", "NULLABLE", - "DIRECTIVE_DEFAULT", "DIRECTIVE_HIDDEN", "DIRECTIVE_OPEN_PARENS", "DIRECTIVE_CLOSE_PARENS", "DIRECTIVE_BANG", - "DIRECTIVE_OP_EQ", "DIRECTIVE_OP_NE", "DIRECTIVE_OP_AND", "DIRECTIVE_OP_OR", "CONDITIONAL_SYMBOL", - }; - - /// - /// Set of antlr type names for CSharp branch keywords. - /// - private static readonly HashSet cSharpBranchKeywords = new() - { - "FOR", "FOREACH", "IF", "SWITCH", "TRY", "WHILE" - }; - - /// - /// Set of antlr type names for CSharp integer and floating point literals. - /// - private static readonly HashSet cSharpNumbers = new() - { - "LITERAL_ACCESS", "INTEGER_LITERAL", "HEX_INTEGER_LITERAL", "BIN_INTEGER_LITERAL", "REAL_LITERAL", "DIGITS" - }; - - /// Set of antlr type names for CSharp character and string literals. - private static readonly HashSet cSharpStrings = new() - { - "CHARACTER_LITERAL", "REGULAR_STRING", "VERBATIUM_STRING", "INTERPOLATED_REGULAR_STRING_START", - "INTERPOLATED_VERBATIUM_STRING_START", "VERBATIUM_DOUBLE_QUOTE_INSIDE", - "DOUBLE_QUOTE_INSIDE", "REGULAR_STRING_INSIDE", "VERBATIUM_INSIDE_STRING" - }; - - /// Set of antlr type names for CSharp separators and operators. - private static readonly HashSet cSharpPunctuation = new() - { - "OPEN_BRACE", "CLOSE_BRACE", "CLOSE_BRACE_INSIDE", "OPEN_BRACKET", - "CLOSE_BRACKET", "OPEN_PARENS", "CLOSE_PARENS", "DOT", "COMMA", "FORMAT_STRING", "COLON", "SEMICOLON", "PLUS", "MINUS", "STAR", "DIV", - "PERCENT", "AMP", "BITWISE_OR", "CARET", "BANG", "TILDE", "ASSIGNMENT", "LT", "GT", "INTERR", "DOUBLE_COLON", - "OP_COALESCING", "OP_INC", "OP_DEC", "OP_AND", "OP_OR", "OP_PTR", "OP_EQ", "OP_NE", "OP_LE", "OP_GE", "OP_ADD_ASSIGNMENT", - "OP_SUB_ASSIGNMENT", "OP_MULT_ASSIGNMENT", "OP_DIV_ASSIGNMENT", "OP_MOD_ASSIGNMENT", "OP_AND_ASSIGNMENT", "OP_OR_ASSIGNMENT", - "OP_XOR_ASSIGNMENT", "OP_LEFT_SHIFT", "OP_LEFT_SHIFT_ASSIGNMENT", "OP_COALESCING_ASSIGNMENT", "OP_RANGE", - "DOUBLE_CURLY_INSIDE", "OPEN_BRACE_INSIDE", "REGULAR_CHAR_INSIDE" - }; - - /// Set of antlr type names for CSharp identifiers. - private static readonly HashSet cSharpIdentifiers = new() - { - "IDENTIFIER", "TEXT" - }; - - /// - /// Set of antlr type names for CSharp whitespace. - /// - private static readonly HashSet cSharpWhitespace = new() - { - "WHITESPACES", "DIRECTIVE_WHITESPACES" - }; - - /// - /// Set of antlr type names for CSharp newlines. - /// - private static readonly HashSet cSharpNewlines = new() - { - "NL", "TEXT_NEW_LINE", "DIRECTIVE_NEW_LINE" - }; - - /// - /// Set of antlr type names for Java comments. - /// - private static readonly HashSet cSharpComments = new() - { - "SINGLE_LINE_DOC_COMMENT", "DELIMITED_DOC_COMMENT", "SINGLE_LINE_COMMENT", "DELIMITED_COMMENT", - "DIRECTIVE_SINGLE_LINE_COMMENT" - }; - - #endregion - - #region CPP Language - - /// - /// Name of the antlr grammar lexer. - /// - private const string cppFileName = "CPP14Lexer.g4"; - - /// - /// Set of CPP file extensions. - /// - private static readonly HashSet cppExtensions = new() - { - "cpp", "cxx", "hpp" - }; - - /// - /// Set of antlr type names for CPP keywords excluding . - /// - private static readonly HashSet cppKeywords = new() - { - "Alignas", "Alignof", "Asm", "Auto", "Bool", "Break", "Case", "Catch", "Continue", - "Char", "Char16", "Char32", "Class", "Const", "Constexpr", "Const_cast", - "Decltype", "Default", "Delete", "Do", "Double", "Dynamic_cast", "Else", - "Enum", "Explicit", "Export", "Extern", "False_", "Final", "Float", - "Friend", "Goto", "Inline", "Int", "Long", "Mutable", "Namespace", - "New", "Noexcept", "Nullptr", "Operator", "Override", "Private", "Protected", - "Public", "Register", "Reinterpret_cast", "Return", "Short", "Signed", - "Sizeof", "Static", "Static_assert", "Static_cast", "Struct", - "Template", "This", "Thread_local", "Throw", "True_", "Typedef", - "Typeid_", "Typename_", "Union", "Unsigned", "Using", "Virtual", "Void", - "Volatile", "Wchar", - "BooleanLiteral", "PointerLiteral", "UserDefinedLiteral", - "MultiLineMacro", "Directive" - }; - - /// - /// Set of antlr type names for CPP branch keywords. - /// - private static readonly HashSet cppBranchKeywords = new() - { - "For", "If", "Switch", "Try", "While" - }; - - /// - /// Set of antlr type names for CPP integer and floating point literals. - /// - private static readonly HashSet cppNumbers = new() - { - "IntegerLiteral", "FloatingLiteral", "DecimalLiteral", "OctalLiteral", "HexadecimalLiteral", - "BinaryLiteral", "Integersuffix", "UserDefinedIntegerLiteral", "UserDefinedFloatingLiteral" - }; - - /// Set of antlr type names for CPP character and string literals. - private static readonly HashSet cppStrings = new() - { - "StringLiteral", "CharacterLiteral", "UserDefinedStringLiteral", "UserDefinedCharacterLiteral" - }; - - /// Set of antlr type names for CPP separators and operators. - private static readonly HashSet cppPunctuation = new() - { - "LeftParen", "RightParen", "LeftBracket", - "RightBracket", "LeftBrace", "RightBrace", "Plus", "Minus", "Star", "Div", - "Mod", "Caret", "And", "Or", "Tilde", "Not", "Assign", "Less", "Greater", - "PlusAssign", "MinusAssign", "StarAssign", "DivAssign", "ModAssign", "XorAssign", - "AndAssign", "OrAssign", "LeftShiftAssign", "RightShiftAssign", "Equal", - "NotEqual", "LessEqual", "GreaterEqual", "AndAnd", "OrOr", "PlusPlus", - "MinusMinus", "Comma", "ArrowStar", "Arrow", "Question", "Colon", "Doublecolon", - "Semi", "Dot", "DotStar", "Ellipsis" - }; - - /// Set of antlr type names for CPP identifiers. - private static readonly HashSet cppIdentifiers = new() { "Identifier" }; - - /// - /// Set of antlr type names for CPP whitespace. - /// - private static readonly HashSet cppWhitespace = new() { "Whitespace" }; - - /// - /// Set of antlr type names for CPP newlines. - /// - private static readonly HashSet cppNewlines = new() { "Newline" }; - - /// - /// Set of antlr type names for CPP comments. - /// - private static readonly HashSet cppComments = new() { "BlockComment", "LineComment" }; - - #endregion - - #region Plain Text "Language" - - /// - /// Name of the antlr grammar lexer. - /// - private const string plainFileName = "PlainTextLexer.g4"; - - /// - /// Set of plain text file extensions. - /// Note that this is a special case, since this is the lexer we'll use when nothing else is available. + /// File extensions which apply for the given language. + /// May not intersect any other languages file extensions. /// - private static readonly HashSet plainExtensions = new(); - - /// Set of antlr type names for keywords. There are none here. - private static readonly HashSet plainKeywords = new(); - - /// Set of antlr type names for branch keywords. There are none here. - private static readonly HashSet plainBranchKeywords = new(); - - /// Set of antlr type names for numbers. - private static readonly HashSet plainNumbers = new(); - - /// Set of antlr type names for character and string literals. There are none here. - private static readonly HashSet plainStrings = new(); - - /// Set of antlr type names for punctuation. - private static readonly HashSet plainPunctuation = new(); - - /// Set of antlr type names for identifiers, which in this case is for normal words. - private static readonly HashSet plainIdentifiers = new() - { - "WORD", "PSEUDOWORD", "LETTERS", "SIGNS", "SPECIAL", "NUMBERS" - }; - - /// Set of antlr type names for whitespace. - private static readonly HashSet plainWhitespace = new() { "WHITESPACES" }; - - /// Set of antlr type names for newlines. - private static readonly HashSet plainNewlines = new() { "NEWLINES" }; - - /// Set of antlr type names for comments. There are none here. - private static readonly HashSet plainComments = new(); - - #endregion - - #region Static Types + public ISet FileExtensions { get; } /// /// A list of all token languages there are. /// - public static readonly IList AllTokenLanguages = new List(); - - /// - /// Token Language for Java. - /// - public static readonly TokenLanguage Java = new(javaFileName, javaExtensions, javaKeywords, javaBranchKeywords, javaNumbers, - javaStrings, javaPunctuation, javaIdentifiers, javaWhitespace, javaNewlines, javaComments); + public static readonly ISet AllTokenLanguages = new HashSet(); - /// - /// Token Language for C#. - /// - public static readonly TokenLanguage CSharp = new(cSharpFileName, cSharpExtensions, cSharpKeywords, cSharpBranchKeywords, cSharpNumbers, - cSharpStrings, cSharpPunctuation, cSharpIdentifiers, cSharpWhitespace, cSharpNewlines, cSharpComments); - - /// - /// Token Language for CPP. - /// - public static readonly TokenLanguage CPP = new(cppFileName, cppExtensions, cppKeywords, cppBranchKeywords, cppNumbers, - cppStrings, cppPunctuation, cppIdentifiers, cppWhitespace, cppNewlines, cppComments); - - /// - /// Token language for plain text. - /// - public static readonly TokenLanguage Plain = new(plainFileName, plainExtensions, plainKeywords, plainBranchKeywords, plainNumbers, - plainStrings, plainPunctuation, plainIdentifiers, plainWhitespace, plainNewlines, plainComments); - - #endregion - - /// - /// Constructor for the token language. - /// - /// Should never be accessible from outside this class. - /// Name of this lexer grammar - /// List of file extensions for this language - /// Keywords of this language - /// Number literals of this language - /// String literals of this language - /// Punctuation for this language - /// Identifiers for this language - /// Whitespace for this language - /// Newlines for this language - /// Comments for this language - /// Branches for this language - /// Number of spaces a tab is equivalent to - private TokenLanguage(string lexerFileName, ISet fileExtensions, ISet keywords, ISet branchKeywords, - ISet numberLiterals, ISet stringLiterals, ISet punctuation, - ISet identifiers, ISet whitespace, ISet newline, - ISet comments, int tabWidth = defaultTabWidth) + protected TokenLanguage(string name, ISet fileExtensions, int tabWidth = defaultTabWidth) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR - if (AllTokenLanguages.Any(x => x.LexerFileName.Equals(lexerFileName) || x.FileExtensions.Overlaps(fileExtensions))) - { - throw new ArgumentException("Lexer file name and file extensions must be unique per language!"); - } - if (AnyOverlaps()) - { - throw new ArgumentException("Symbolic names may not appear in more than one set each!"); - } -#endif - LexerFileName = lexerFileName; - FileExtensions = fileExtensions; - Keywords = keywords; - BranchKeywords = branchKeywords; - NumberLiterals = numberLiterals; - StringLiterals = stringLiterals; - Punctuation = punctuation; - Identifiers = identifiers; - Whitespace = whitespace; - Newline = newline; - Comments = comments; + Name = name; TabWidth = tabWidth; - + FileExtensions = fileExtensions; AllTokenLanguages.Add(this); - - // Check whether any of the symbolic names are used twice - bool AnyOverlaps() - { - return keywords.Intersect(numberLiterals).Intersect(stringLiterals).Intersect(punctuation) - .Intersect(identifiers).Intersect(whitespace).Intersect(newline) - .Intersect(comments).Intersect(branchKeywords).Any(); - } - } - - /// - /// Returns the matching token language for the given . - /// If no matching token language is found, an exception will be thrown. - /// - /// File name of the antlr lexer. Can be found in lexer.GrammarFileName - /// The matching token language - /// If the given is not supported. - public static TokenLanguage FromLexerFileName(string lexerFileName) - { - return AllTokenLanguages.SingleOrDefault(x => x.LexerFileName.Equals(lexerFileName)) - ?? throw new ArgumentException($"The given {nameof(lexerFileName)} is not of a supported grammar. Supported grammars are " - + string.Join(", ", AllTokenLanguages.Select(x => x.LexerFileName))); - } - - /// - /// Returns the matching token language for the given . - /// If no matching token language is found, the will be used, unless - /// is true. - /// - /// File extension for the language. - /// - /// Whether to throw an exception when an unknown file extension is encountered. - /// If this is false, the will be used instead in such a case. - /// - /// The matching token language. - /// - /// If the given is not supported and is true. - /// - public static TokenLanguage FromFileExtension(string extension, bool throwOnUnknown = false) - { - TokenLanguage target = AllTokenLanguages.SingleOrDefault(x => x.FileExtensions.Contains(extension)); - if (target == null) - { - if (throwOnUnknown) - { - throw new ArgumentException("The given filetype is not supported. Supported filetypes are " - + string.Join(", ", AllTokenLanguages.SelectMany(x => x.FileExtensions))); - } - - target = Plain; - } - - return target; - } - - /// - /// Creates a new lexer matching the of this language. - /// - /// The string which shall be parsed by the lexer. - /// the new matching lexer - /// If no lexer is defined for this language. - public Lexer CreateLexer(string content) - { - ICharStream input = CharStreams.fromString(content); - return LexerFileName switch - { - javaFileName => new Java9Lexer(input), - cSharpFileName => new CSharpLexer(input), - cppFileName => new CPP14Lexer(input), - plainFileName => new PlainTextLexer(input), - _ => throw new InvalidOperationException("No lexer defined for this language yet.") - }; - } - - /// - /// Returns the type of token this is. - /// The type of token will be represented by the name of the collection it is in. - /// Returns null if the token is not any known type. - /// - /// a symbolic name from the antlr lexer for this language - /// name of the type the given is, or null if it isn't known. - public string TypeName(string token) - { - // We go through each category and check whether it contains the token. - // I know that this looks like it may be abstracted because the same thing is done on different objects - // in succession, but due to the usage of nameof() a refactoring of this kind would break it. - if (Keywords.Contains(token)) - { - return nameof(Keywords); - } - if (BranchKeywords.Contains(token)) - { - return nameof(BranchKeywords); - } - if (NumberLiterals.Contains(token)) - { - return nameof(NumberLiterals); - } - if (StringLiterals.Contains(token)) - { - return nameof(StringLiterals); - } - if (Punctuation.Contains(token)) - { - return nameof(Punctuation); - } - if (Identifiers.Contains(token)) - { - return nameof(Identifiers); - } - if (Comments.Contains(token)) - { - return nameof(Comments); - } - if (Whitespace.Contains(token)) - { - return nameof(Whitespace); - } - if (Newline.Contains(token)) - { - return nameof(Newline); - } - return eof.Equals(token) ? nameof(eof) : null; } } } diff --git a/Assets/SEE/Scanner/TokenLanguage.cs.meta b/Assets/SEE/Scanner/TokenLanguage.cs.meta index 15189a1054..39e810c628 100644 --- a/Assets/SEE/Scanner/TokenLanguage.cs.meta +++ b/Assets/SEE/Scanner/TokenLanguage.cs.meta @@ -1,3 +1,3 @@ -fileFormatVersion: 2 -guid: 097994b6864d45cdb3146b812d865ae5 -timeCreated: 1620241512 \ No newline at end of file +fileFormatVersion: 2 +guid: cfe4319940e64ab8b2426e1dc3ef4d7c +timeCreated: 1719433094 \ No newline at end of file diff --git a/Assets/SEE/Scanner/TokenMetrics.cs b/Assets/SEE/Scanner/TokenMetrics.cs index 505f2715e6..c9bef5f57f 100644 --- a/Assets/SEE/Scanner/TokenMetrics.cs +++ b/Assets/SEE/Scanner/TokenMetrics.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using SEE.Scanner.Antlr; using UnityEngine; namespace SEE.Scanner @@ -14,12 +15,12 @@ public static class TokenMetrics /// /// The tokens used for which the complexity should be calculated. /// Returns the McCabe cyclomatic complexity. - public static int CalculateMcCabeComplexity(IEnumerable tokens) + public static int CalculateMcCabeComplexity(IEnumerable tokens) { int complexity = 1; // Starting complexity for a single method or function. // Count decision points (branches). - complexity += tokens.Count(t => t.TokenType == SEEToken.Type.BranchKeyword); + complexity += tokens.Count(t => t.TokenType == AntlrTokenType.BranchKeyword); return complexity; } @@ -59,33 +60,33 @@ float NumberOfDeliveredBugs /// /// The tokens for which the metrics should be calculated. /// Returns the Halstead metrics. - public static HalsteadMetrics CalculateHalsteadMetrics(IEnumerable tokens) + public static HalsteadMetrics CalculateHalsteadMetrics(ICollection tokens) { // Set of token types which are operands. - HashSet operandTypes = new() + HashSet operandTypes = new() { - SEEToken.Type.Identifier, - SEEToken.Type.Keyword, - SEEToken.Type.BranchKeyword, - SEEToken.Type.NumberLiteral, - SEEToken.Type.StringLiteral + AntlrTokenType.Identifier, + AntlrTokenType.Keyword, + AntlrTokenType.BranchKeyword, + AntlrTokenType.NumberLiteral, + AntlrTokenType.StringLiteral }; // Identify operands. HashSet operands = new(tokens.Where(t => operandTypes.Contains(t.TokenType)).Select(t => t.Text)); // Identify operators. - HashSet operators = new(tokens.Where(t => t.TokenType == SEEToken.Type.Punctuation).Select(t => t.Text)); + HashSet operators = new(tokens.Where(t => t.TokenType == AntlrTokenType.Punctuation).Select(t => t.Text)); // Count the total number of operands and operators. int totalOperands = tokens.Count(t => operandTypes.Contains(t.TokenType)); - int totalOperators = tokens.Count(t => t.TokenType == SEEToken.Type.Punctuation); + int totalOperators = tokens.Count(t => t.TokenType == AntlrTokenType.Punctuation); // Derivative Halstead metrics. int programVocabulary = operators.Count + operands.Count; int programLength = totalOperators + totalOperands; - float estimatedProgramLength = operators.Count == 0 ? 0 : (float)((operators.Count * Mathf.Log(operators.Count, 2) + operands.Count * Mathf.Log(operands.Count, 2))); - float volume = programVocabulary == 0 ? 0 : (float)(programLength * Mathf.Log(programVocabulary, 2)); + float estimatedProgramLength = operators.Count == 0 ? 0 : operators.Count * Mathf.Log(operators.Count, 2) + operands.Count * Mathf.Log(operands.Count, 2); + float volume = programVocabulary == 0 ? 0 : programLength * Mathf.Log(programVocabulary, 2); float difficulty = operands.Count == 0 ? 0 : operators.Count / 2.0f * (totalOperands / (float)operands.Count); float effort = difficulty * volume; float timeRequiredToProgram = effort / 18.0f; // Formula: Time T = effort E / S, where S = Stroud's number of psychological 'moments' per second; typically a figure of 18 is used in Software Science. @@ -112,25 +113,25 @@ public static HalsteadMetrics CalculateHalsteadMetrics(IEnumerable tok ///
/// The tokens for which the lines of code should be counted. /// Returns the number of lines of code. - public static int CalculateLinesOfCode(IEnumerable tokens) + public static int CalculateLinesOfCode(IEnumerable tokens) { int linesOfCode = 0; bool comment = false; - foreach (SEEToken token in tokens) + foreach (AntlrToken token in tokens) { - if (token.TokenType == SEEToken.Type.Newline) + if (token.TokenType == TokenType.Newline) { if (!comment) { linesOfCode++; } } - else if (token.TokenType == SEEToken.Type.Comment) + else if (token.TokenType == AntlrTokenType.Comment) { comment = true; } - else if (token.TokenType != SEEToken.Type.Whitespace) + else if (token.TokenType != TokenType.Whitespace) { comment = false; } @@ -138,4 +139,4 @@ public static int CalculateLinesOfCode(IEnumerable tokens) return linesOfCode; } } -} \ No newline at end of file +} diff --git a/Assets/SEE/Scanner/TokenModifiers.cs b/Assets/SEE/Scanner/TokenModifiers.cs new file mode 100644 index 0000000000..5ab837fbe6 --- /dev/null +++ b/Assets/SEE/Scanner/TokenModifiers.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using SEE.Utils; + +namespace SEE.Scanner +{ + /// + /// Modifiers that can be applied to a semantic token + /// (for example, or ). + /// + [Flags] + public enum TokenModifiers + { + /// + /// No modifiers. + /// + None = 0, + + /// + /// For declarations of symbols. + /// + Declaration = 1 << 0, + + /// + /// For definitions of symbols, for example, in header files. + /// + Definition = 1 << 1, + + /// + /// For readonly variables and member fields, as well as constants. + /// + Readonly = 1 << 2, + + /// + /// For class members that are static. + /// + Static = 1 << 3, + + /// + /// For symbols that should no longer be used. + /// + Deprecated = 1 << 4, + + /// + /// For types and member functions that are abstract. + /// + Abstract = 1 << 5, + + /// + /// For functions that are marked as asynchronous. + /// + Async = 1 << 6, + + /// + /// For variable references where the variable is reassigned. + /// + Modification = 1 << 7, + + /// + /// For occurrences of symbols in documentation. + /// + Documentation = 1 << 8, + + /// + /// For symbols that are part of the standard library. + /// + DefaultLibrary = 1 << 9 + } + + /// + /// Extension methods for . + /// + public static class TokenModifiersExtensions + { + /// + /// Mapping from to . + /// + private static readonly IDictionary tokenModifierMapping = new Dictionary + { + { SemanticTokenModifier.Declaration, TokenModifiers.Declaration }, + { SemanticTokenModifier.Definition, TokenModifiers.Definition }, + { SemanticTokenModifier.Readonly, TokenModifiers.Readonly }, + { SemanticTokenModifier.Static, TokenModifiers.Static }, + { SemanticTokenModifier.Deprecated, TokenModifiers.Deprecated }, + { SemanticTokenModifier.Abstract, TokenModifiers.Abstract }, + { SemanticTokenModifier.Async, TokenModifiers.Async }, + { SemanticTokenModifier.Modification, TokenModifiers.Modification }, + { SemanticTokenModifier.Documentation, TokenModifiers.Documentation }, + { SemanticTokenModifier.DefaultLibrary, TokenModifiers.DefaultLibrary } + }; + + /// + /// Converts a to a . + /// + /// The to convert. + /// The that corresponds to the given . + public static TokenModifiers FromLspTokenModifier(this SemanticTokenModifier modifier) + { + return tokenModifierMapping.GetValueOrDefault(modifier); + } + + /// + /// Converts a to the name of a tag that can be used in + /// TextMeshPro's rich text markup. + /// + /// The to convert. Should be a single flag. + /// The name of a tag that can be used in TextMeshPro's rich text markup. + public static string ToRichTextTag(this TokenModifiers modifiers) + { + return modifiers switch + { + TokenModifiers.Static => "i", + TokenModifiers.Deprecated => "strikethrough", + TokenModifiers.Modification => "u", + TokenModifiers.Documentation => "i", + _ => string.Empty + }; + } + + /// + /// Returns a stream of that are set in the given . + /// + /// The to get the set modifiers from. + /// An enumerable of that are set in the given . + public static IEnumerable AsEnumerable(this TokenModifiers modifiers) + { + return Enum.GetValues(typeof(TokenModifiers)).Cast().Where(modifier => modifiers.HasFlag(modifier)); + } + } +} diff --git a/Assets/SEE/Scanner/TokenModifiers.cs.meta b/Assets/SEE/Scanner/TokenModifiers.cs.meta new file mode 100644 index 0000000000..ed2958c94e --- /dev/null +++ b/Assets/SEE/Scanner/TokenModifiers.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 37ae1a4897954349b570caad73383d4f +timeCreated: 1719516779 \ No newline at end of file diff --git a/Assets/SEE/Scanner/TokenType.cs b/Assets/SEE/Scanner/TokenType.cs new file mode 100644 index 0000000000..d743b9a987 --- /dev/null +++ b/Assets/SEE/Scanner/TokenType.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +namespace SEE.Scanner +{ + /// + /// Represents a kind of token in a programming language, with an associated color. + /// + public record TokenType + { + #region Static TokenTypes + + /// + /// List of all token types. + /// + protected static readonly IList AllTokens = new List(); + + /// + /// Whitespace tokens, excluding newlines. + /// + public static readonly TokenType Whitespace = new("Whitespace", "#000000"); // color doesn't matter + + /// + /// Newline tokens. Must contain exactly one newline. + /// + public static readonly TokenType Newline = new("Newline", "#000000"); // color doesn't matter + + /// + /// End-Of-File token. + /// + public static readonly TokenType EOF = new("eof", "#000000"); // color doesn't matter + + #endregion + + protected TokenType(string name, string color) + { + Name = name; + Color = color.TrimStart('#'); + + AllTokens.Add(this); + } + + /// Name of the token type. + public string Name { get; } + + /// + /// Color the token type should be rendered in hexadecimal RGB notation (no '#' sign). + /// An optional fourth byte may be entered to define the alpha value. + /// + public string Color { get; } + } +} diff --git a/Assets/SEE/Scanner/TokenType.cs.meta b/Assets/SEE/Scanner/TokenType.cs.meta new file mode 100644 index 0000000000..3f5c0ae2d1 --- /dev/null +++ b/Assets/SEE/Scanner/TokenType.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 27b3e2c6cd244e0baa0aa0f826b6f4bf +timeCreated: 1719426272 \ No newline at end of file diff --git a/Assets/SEE/Tools/LSP/LSPHandler.cs b/Assets/SEE/Tools/LSP/LSPHandler.cs index e9164a389d..797191d30c 100644 --- a/Assets/SEE/Tools/LSP/LSPHandler.cs +++ b/Assets/SEE/Tools/LSP/LSPHandler.cs @@ -76,7 +76,13 @@ public LSPServer Server /// Whether to log the communication between the language server and SEE to a temporary file. /// [field: SerializeField, HideInInspector] - public bool LogLSP { get; set; } + public bool LogLSP { get; set; } = true; + + /// + /// Whether to use LSP capabilities in code windows. + /// + [field: SerializeField, HideInInspector] + public bool UseInCodeWindows { get; set; } /// /// The language client that is used to communicate with the language server. @@ -103,6 +109,11 @@ public LSPServer Server /// public TimeSpan TimeoutSpan = TimeSpan.FromSeconds(2); + /// + /// The URI of the project. + /// + public Uri ProjectUri => new(ProjectPath, UriKind.Absolute); + /// /// The capabilities of the language server. /// @@ -113,6 +124,11 @@ public LSPServer Server /// private readonly ConcurrentQueue unhandledDiagnostics = new(); + /// + /// A dictionary mapping from file paths to the diagnostics that have been published for that file. + /// + private readonly Dictionary> savedDiagnostics = new(); + /// /// The capabilities of the language client. /// @@ -159,8 +175,74 @@ public LSPServer Server }, LabelSupport = false }, - Diagnostic = new DiagnosticClientCapabilities(), - PublishDiagnostics = new PublishDiagnosticsCapability() + Diagnostic = new DiagnosticClientCapabilities + { + RelatedDocumentSupport = true + }, + PublishDiagnostics = new PublishDiagnosticsCapability + { + RelatedInformation = true, + VersionSupport = false, + TagSupport = new Supports(new PublishDiagnosticsTagSupportCapabilityOptions + { + ValueSet = Container.From(DiagnosticTag.Unnecessary, DiagnosticTag.Deprecated) + }) + }, + SemanticTokens = new SemanticTokensCapability() + { + Requests = new SemanticTokensCapabilityRequests() + { + Full = new Supports() + }, + Formats = new[] + { + SemanticTokenFormat.Relative + }, + TokenModifiers = new[] + { + SemanticTokenModifier.Deprecated, + SemanticTokenModifier.Static, + SemanticTokenModifier.Abstract, + SemanticTokenModifier.Readonly, + SemanticTokenModifier.Async, + SemanticTokenModifier.Declaration, + SemanticTokenModifier.Definition, + SemanticTokenModifier.Documentation, + SemanticTokenModifier.Modification, + SemanticTokenModifier.DefaultLibrary + }, + TokenTypes = new[] + { + SemanticTokenType.Comment, + SemanticTokenType.Keyword, + SemanticTokenType.String, + SemanticTokenType.Number, + SemanticTokenType.Regexp, + SemanticTokenType.Operator, + SemanticTokenType.Namespace, + SemanticTokenType.Type, + SemanticTokenType.Struct, + SemanticTokenType.Class, + SemanticTokenType.Interface, + SemanticTokenType.Enum, + SemanticTokenType.TypeParameter, + SemanticTokenType.Function, + SemanticTokenType.Method, + SemanticTokenType.Property, + SemanticTokenType.Macro, + SemanticTokenType.Variable, + SemanticTokenType.Parameter, + SemanticTokenType.Label, + SemanticTokenType.Modifier, + SemanticTokenType.Event, + SemanticTokenType.EnumMember, + SemanticTokenType.Decorator + }, + OverlappingTokenSupport = false, + MultilineTokenSupport = false, + ServerCancelSupport = false, + AugmentsSyntaxTokens = false + } }, Window = new WindowClientCapabilities { @@ -197,6 +279,8 @@ public async UniTask InitializeAsync(string executablePath = null, CancellationT return; } + savedDiagnostics.Clear(); + unhandledDiagnostics.Clear(); HashSet initialWork = new(); IDisposable spinner = LoadingSpinner.ShowIndeterminate("Starting language server..."); try @@ -305,24 +389,26 @@ void HandleInitialWorkDoneProgress(WorkDoneProgressCreateParams progressParams) } } - async UniTaskVoid MonitorInitialWorkDoneProgress(ProgressToken token) + async UniTaskVoid MonitorInitialWorkDoneProgress(ProgressToken progressToken) { - await foreach (WorkDoneProgress _ in Client.WorkDoneManager.Monitor(token).ToUniTaskAsyncEnumerable() + await foreach (WorkDoneProgress _ in Client.WorkDoneManager.Monitor(progressToken).ToUniTaskAsyncEnumerable() .Where(x => x.Kind == WorkDoneProgressKind.End)) { - initialWork.Remove(token); + initialWork.Remove(progressToken); } } } /// /// Handles the diagnostics published by the language server by storing them - /// in the queue. + /// in the queue, as well as + /// in the dictionary. /// /// The parameters of the diagnostics. private void HandleDiagnostics(PublishDiagnosticsParams diagnosticsParams) { unhandledDiagnostics.Enqueue(diagnosticsParams); + savedDiagnostics.GetOrAdd(diagnosticsParams.Uri.GetFileSystemPath(), () => new()).Add(diagnosticsParams); } /// @@ -332,6 +418,12 @@ private void HandleDiagnostics(PublishDiagnosticsParams diagnosticsParams) /// The parameters of the ShowMessage notification. private void ShowMessage(ShowMessageParams showMessageParams) { + if (showMessageParams.Message.Contains("window/workDoneProgress/cancel")) + { + // Cancellation messages are sometimes sent to the language server even when they don't support them. + // We can safely ignore any failing cancellations. + return; + } string languageServerName = Server?.Name ?? "Language Server"; switch (showMessageParams.Type) { @@ -449,7 +541,7 @@ public async UniTask HoverAsync(string path, int line, int character = 0) /// compared to the last call, the method returns null. /// /// Note that this is a very new feature (LSP 3.17) and not all language servers support it. - /// An alternative is to use the method to + /// An alternative is to use the method to /// retrieve the diagnostics that have been published by the language server. /// /// The path to the document. @@ -469,8 +561,8 @@ public async UniTask> PullDocumentDiagnosticsAsync(strin /// /// Retrieves the unhandled diagnostics that have been published by the language server. /// - /// An enumerable of the published diagnostics. - public IEnumerable GetPublishedDiagnostics() + /// An enumerable of the unhandled published diagnostics. + public IEnumerable GetUnhandledPublishedDiagnostics() { while (unhandledDiagnostics.TryDequeue(out PublishDiagnosticsParams diagnostics)) { @@ -478,6 +570,18 @@ public IEnumerable GetPublishedDiagnostics() } } + /// + /// Returns the diagnostics that were saved for the given . + /// Note that this may not include every diagnostic the language server would have sent, + /// as we only listen to published diagnostics for a certain timeframe (see ). + /// + /// The path for which to retrieve the diagnostics. + /// The published diagnostics for the given path. + public IEnumerable GetPublishedDiagnosticsForPath(string path) + { + return savedDiagnostics.GetValueOrDefault(path) ?? Enumerable.Empty(); + } + /// /// Retrieves all references to the symbol in the document with the given at the given /// and . @@ -580,8 +684,20 @@ public IUniTaskAsyncEnumerable OutgoingCalls(Func Client.RequestCallHierarchyOutgoing(outgoingParams, t), TimeoutSpan).Select(x => x.To); + // We can not use the built-in method here and have to make the request manually, + // as the specialized method contains a bug (issue #1303 in OmniSharp/csharp-language-server-protocol). + // return AsyncUtils.ObserveUntilTimeout(t => Client.RequestCallHierarchyOutgoing(outgoingParams, t), TimeoutSpan).Select(x => x.To); + return AsyncUtils.RunWithTimeoutAsync(MakeOutgoingCallRequest(outgoingParams), TimeoutSpan) + .AsUniTaskAsyncEnumerable() + .Select(y => y.To); }); + + Func>> MakeOutgoingCallRequest(CallHierarchyOutgoingCallsParams outgoingParams) + { + return token => Client.SendRequest("callHierarchy/outgoingCalls", outgoingParams) + .Returning>(token) + .AsUniTask(useCurrentSynchronizationContext: false); + } } /// @@ -638,6 +754,22 @@ private IUniTaskAsyncEnumerable GetLocationsByLspFunc(string path, int return AsyncUtils.ObserveUntilTimeout(t => lspFunction(parameters, t), TimeoutSpan); } + /// + /// Retrieves semantic tokens for the document at the given . + /// + /// Note that the returned semantic tokens may be empty if the document has not been fully analyzed yet. + /// + /// The path to the document. + /// The semantic tokens for the document at the given path. + public async UniTask GetSemanticTokensAsync(string path) + { + SemanticTokensParams parameters = new() + { + TextDocument = new TextDocumentIdentifier(path) + }; + return await Client.RequestSemanticTokensFull(parameters); + } + /// /// Shuts down the language server and exits its process. /// diff --git a/Assets/SEE/Tools/LSP/LSPIssue.cs b/Assets/SEE/Tools/LSP/LSPIssue.cs new file mode 100644 index 0000000000..abec60f12e --- /dev/null +++ b/Assets/SEE/Tools/LSP/LSPIssue.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using SEE.UI.Window.CodeWindow; +using Range = SEE.DataModel.DG.Range; + +namespace SEE.Tools.LSP +{ + /// + /// A code issue diagnosed by a language server. + /// + /// The path of the file where the issue was diagnosed. + /// The diagnostic that represents the issue. + public record LSPIssue(string Path, Diagnostic Diagnostic) : IDisplayableIssue + { + /// + /// Implements . + /// + public UniTask ToDisplayStringAsync() + { + string message = ""; + if (Diagnostic.Code.HasValue) + { + message += $"{Diagnostic.Code.Value.String ?? Diagnostic.Code.Value.Long.ToString()}: "; + } + message += $"{Diagnostic.Message}"; + return UniTask.FromResult(message); + } + + /// + /// Implements . + /// + public string Source => Diagnostic.Source ?? "LSP"; + + /// + /// Implements . + /// + public IList RichTags + { + get + { + List tags = Diagnostic.Tags?.ToList() ?? new(); + if (tags.Count > 0) + { + return tags.Select(DiagnosticTagToRichTag).ToList(); + } + else + { + // If there are no explicit tags, we create a tag based on the severity. + return new List + { + DiagnosticSeverityToTag(Diagnostic.Severity ?? DiagnosticSeverity.Warning) + }; + } + } + } + + /// + /// Converts a diagnostic tag to a TextMeshPro rich text tag, intended to be used within code windows. + /// + /// The diagnostic tag to convert. + /// The TextMeshPro rich text tag that corresponds to the given . + private static string DiagnosticTagToRichTag(DiagnosticTag tag) => + tag switch + { + DiagnosticTag.Unnecessary => "", + DiagnosticTag.Deprecated => "", + _ => throw new ArgumentOutOfRangeException(nameof(tag), tag, "Unknown diagnostic tag") + }; + + /// + /// Converts a diagnostic severity to a TextMeshPro rich text tag, intended to be used within code windows. + /// + /// The diagnostic severity to convert. + /// The TextMeshPro rich text tag that corresponds to the given . + private static string DiagnosticSeverityToTag(DiagnosticSeverity severity) => + severity switch + { + DiagnosticSeverity.Error => "", + DiagnosticSeverity.Warning => "", + DiagnosticSeverity.Information => "", + DiagnosticSeverity.Hint => "", + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, "Unknown diagnostic severity") + }; + + /// + /// Implements . + /// + public IEnumerable<(string Path, Range Range)> Occurrences + { + get + { + List<(string Path, Range Range)> occurrences = new() + { + (Path, Range.FromLspRange(Diagnostic.Range)) + }; + if (Diagnostic.RelatedInformation != null) + { + occurrences.AddRange(Diagnostic.RelatedInformation.Select(x => (x.Location.Uri.GetFileSystemPath(), Range.FromLspRange(x.Location.Range)))); + } + return occurrences; + } + } + } +} diff --git a/Assets/SEE/Tools/LSP/LSPIssue.cs.meta b/Assets/SEE/Tools/LSP/LSPIssue.cs.meta new file mode 100644 index 0000000000..5f566e2bb2 --- /dev/null +++ b/Assets/SEE/Tools/LSP/LSPIssue.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0f8ef404e0e848dbacec41015bca6a4e +timeCreated: 1720786567 \ No newline at end of file diff --git a/Assets/SEE/Tools/LSP/LSPLanguage.cs b/Assets/SEE/Tools/LSP/LSPLanguage.cs index f38bdb7e53..53e79be483 100644 --- a/Assets/SEE/Tools/LSP/LSPLanguage.cs +++ b/Assets/SEE/Tools/LSP/LSPLanguage.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SEE.Scanner; namespace SEE.Tools.LSP { @@ -8,18 +9,8 @@ namespace SEE.Tools.LSP /// A programming language supported by a language server. /// /// - public record LSPLanguage + public class LSPLanguage: TokenLanguage { - /// - /// The name of the language. - /// - public string Name { get; } - - /// - /// The file extensions associated with this language. - /// - public ISet Extensions { get; } - /// /// A mapping from file extensions to LSP language IDs. /// @@ -33,16 +24,13 @@ public record LSPLanguage /// The name of the language. /// The file extensions associated with this language. /// A mapping from file extensions to LSP language IDs. - private LSPLanguage(string name, ISet extensions, IDictionary languageIds = null) + private LSPLanguage(string name, ISet extensions, IDictionary languageIds = null): base(name, extensions) { if (name.Contains('/')) { throw new ArgumentException("Language name must not contain slashes!"); } - Name = name; - Extensions = extensions; LanguageIds = languageIds ?? new Dictionary(); - All.Add(this); } /// @@ -63,7 +51,7 @@ private LSPLanguage(string name, ISet extensions, string languageId) : t /// The language with the given . public static LSPLanguage GetByName(string name) { - return All.First(language => language.Name == name); + return AllLspLanguages.First(language => language.Name == name); } public override string ToString() @@ -71,10 +59,6 @@ public override string ToString() return Name; } - // NOTE: All servers below have been tested first. Before adding a language server to this list, - // please make sure that it actually works in SEE, since we have some special requirements - // (e.g., we require a documentSymbol provider that returns hierarchic `DocumentSymbol` objects). - public static readonly IList All = new List(); public static readonly LSPLanguage C = new("C", new HashSet { "c", "h" }, "c"); public static readonly LSPLanguage CPP = new("C++", new HashSet { @@ -111,5 +95,10 @@ public override string ToString() }, "typescript"); public static readonly LSPLanguage XML = new("XML", new HashSet { "xml", "gxl" }, "xml"); public static readonly LSPLanguage Zig = new("Zig", new HashSet { "zig" }, "zig"); + + /// + /// A list of all supported LSP languages. + /// + public static readonly IList AllLspLanguages = AllTokenLanguages.OfType().ToList(); } } diff --git a/Assets/SEE/Tools/LSP/LSPServer.cs b/Assets/SEE/Tools/LSP/LSPServer.cs index 7928b5f81e..9081b8a500 100644 --- a/Assets/SEE/Tools/LSP/LSPServer.cs +++ b/Assets/SEE/Tools/LSP/LSPServer.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using SEE.Utils; +using static SEE.Tools.LSP.LSPLanguage; namespace SEE.Tools.LSP { @@ -75,7 +76,7 @@ private LSPServer(string name, string websiteURL, IList languages, /// The language ID for the given file . public string LanguageIdFor(string extension) { - LSPLanguage language = Languages.FirstOrDefault(language => language.Extensions.Contains(extension)); + LSPLanguage language = Languages.FirstOrDefault(language => language.FileExtensions.Contains(extension)); return language?.LanguageIds.GetValueOrDefault(extension, language.LanguageIds.GetValueOrDefault(string.Empty)); } @@ -86,69 +87,73 @@ public override string ToString() public static readonly IList All = new List(); + // NOTE: All servers below have been tested first. Before adding a language server to this list, + // please make sure that it actually works in SEE, since we have some special requirements + // (e.g., we require a documentSymbol provider that returns hierarchic `DocumentSymbol` objects). + public static readonly LSPServer Clangd = new("clangd", "https://clangd.llvm.org/", - new List { LSPLanguage.C, LSPLanguage.CPP }, + new List { C, CPP }, "clangd", "--background-index"); public static readonly LSPServer Gopls = new("gopls", "https://github.com/golang/tools/tree/master/gopls", - new List { LSPLanguage.Go }, + new List { Go }, "gopls"); public static readonly LSPServer HaskellLanguageServer = new("Haskell Language Server", "https://haskell-language-server.readthedocs.io/en/latest/", - new List { LSPLanguage.Haskell }, + new List { Haskell }, "haskell-language-server", "--lsp"); public static readonly LSPServer EclipseJdtls = new("Eclipse JDT Language Server", "https://github.com/eclipse-jdtls/eclipse.jdt.ls", - new List { LSPLanguage.Java }, + new List { Java }, "jdtls"); public static readonly LSPServer TypescriptLanguageServer = new("Typescript Language Server", "https://github.com/typescript-language-server/typescript-language-server", - new List { LSPLanguage.TypeScript, LSPLanguage.JavaScript }, + new List { TypeScript, JavaScript }, "typescript-language-server", "--stdio"); public static readonly LSPServer VscodeJson = new("VSCode JSON Language Server", "https://www.npmjs.com/package/vscode-json-languageserver", - new List { LSPLanguage.JSON }, + new List { JSON }, "vscode-json-languageserver", "--stdio"); public static readonly LSPServer Texlab = new("Texlab", "https://github.com/latex-lsp/texlab", - new List { LSPLanguage.LaTeX }, + new List { LaTeX }, "texlab"); public static readonly LSPServer LuaLanguageServer = new("Lua Language Server", "https://github.com/LuaLS/lua-language-server", - new List { LSPLanguage.Lua }, + new List { Lua }, "lua-language-server"); public static readonly LSPServer Marksman = new("Marksman", "https://github.com/artempyanykh/marksman", - new List { LSPLanguage.Markdown }, + new List { Markdown }, "marksman", "server"); public static readonly LSPServer MatlabLanguageServer = new("Matlab Language Server", "https://github.com/mathworks/MATLAB-language-server", - new List { LSPLanguage.MATLAB }, + new List { MATLAB }, "matlab-language-server", "--stdio"); public static readonly LSPServer PhpActor = new("Phpactor", "https://github.com/phpactor/phpactor", - new List { LSPLanguage.PHP }, + new List { PHP }, "phpactor", "language-server"); public static readonly LSPServer Intelephense = new("Intelephense", "https://github.com/bmewburn/vscode-intelephense", - new List { LSPLanguage.PHP }, + new List { PHP }, "intelephense", "--stdio"); public static readonly LSPServer Omnisharp = new("Omnisharp", "https://github.com/OmniSharp/omnisharp-roslyn", - new List { LSPLanguage.CSharp }, + new List { CSharp }, "omnisharp", "-z DotNet:enablePackageRestore=false -e utf-8 -lsp", initOptions: new Dictionary { @@ -164,33 +169,33 @@ public override string ToString() public static readonly LSPServer DartAnalysisServer = new("Dart analysis server", "https://github.com/dart-lang/sdk/blob/master/pkg/analysis_server/tool/lsp_spec/README.md", - new List { LSPLanguage.Dart }, + new List { Dart }, "dart", "language-server"); public static readonly LSPServer KotlinLanguageServer = new("Kotlin Language Server", "https://github.com/fwcd/kotlin-language-server", - new List { LSPLanguage.Kotlin }, + new List { Kotlin }, "kotlin-language-server"); public static readonly LSPServer Pyright = new("Pyright", "https://github.com/microsoft/pyright", - new List { LSPLanguage.Python }, + new List { Python }, "pyright-langserver", "--stdio"); public static readonly LSPServer Jedi = new("Jedi Language Server", "https://github.com/pappasam/jedi-language-server", - new List { LSPLanguage.Python }, + new List { Python }, "jedi-language-server"); public static readonly LSPServer RubyLsp = new("Ruby LSP", "https://github.com/Shopify/ruby-lsp", - new List { LSPLanguage.Ruby }, + new List { Ruby }, "srb", "typecheck --lsp --disable-watchman ."); public static readonly LSPServer RustAnalyzer = new("Rust Analyzer", "https://github.com/rust-lang/rust-analyzer", - new List { LSPLanguage.Rust }, + new List { Rust }, "rust-analyzer", initOptions: new Dictionary { @@ -204,12 +209,12 @@ public override string ToString() public static readonly LSPServer Lemminx = new("Lemminx", "https://github.com/eclipse/lemminx", - new List { LSPLanguage.XML }, + new List { XML }, "lemminx"); public static readonly LSPServer ZLS = new("ZLS", "https://github.com/zigtools/zls", - new List { LSPLanguage.Zig }, + new List { Zig }, "zls"); /// diff --git a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSession.cs b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSession.cs index d11acae1cb..dd4249a142 100644 --- a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSession.cs +++ b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSession.cs @@ -336,11 +336,11 @@ private void OnDestroy() { Destroyer.Destroy(controls); } - if (adapterProcess != null && !adapterProcess.HasExited) + if (adapterProcess is { HasExited: false }) { adapterProcess.Kill(); } - if (adapterHost != null && adapterHost.IsRunning) + if (adapterHost is { IsRunning: true }) { adapterHost.Stop(); } @@ -349,12 +349,12 @@ private void OnDestroy() /// /// Creates the process for the debug adapter. /// - /// Whether the creation was sucessful. + /// Whether the creation was successful. private bool CreateAdapterProcess() { adapterProcess = new Process { - StartInfo = new ProcessStartInfo() + StartInfo = new ProcessStartInfo { FileName = Adapter.AdapterFileName, Arguments = Adapter.AdapterArguments, @@ -371,8 +371,8 @@ private bool CreateAdapterProcess() }, EnableRaisingEvents = true }; - adapterProcess.Exited += (_, args) => ConsoleWindow.AddMessage($"Process: Exited! ({(!adapterProcess.HasExited ? adapterProcess.ProcessName : null)})"); - adapterProcess.Disposed += (_, args) => ConsoleWindow.AddMessage($"Process: Exited! ({(!adapterProcess.HasExited ? adapterProcess.ProcessName : null)})"); + adapterProcess.Exited += (_, _) => ConsoleWindow.AddMessage($"Process: Exited! ({(!adapterProcess.HasExited ? adapterProcess.ProcessName : null)})"); + adapterProcess.Disposed += (_, _) => ConsoleWindow.AddMessage($"Process: Exited! ({(!adapterProcess.HasExited ? adapterProcess.ProcessName : null)})"); adapterProcess.OutputDataReceived += (_, args) => ConsoleWindow.AddMessage($"Process: OutputDataReceived! ({adapterProcess.ProcessName})\n\t{args.Data}"); adapterProcess.ErrorDataReceived += (_, args) => { @@ -410,7 +410,7 @@ private bool CreateAdapterProcess() private bool CreateAdapterHost() { adapterHost = new DebugProtocolHost(adapterProcess.StandardInput.BaseStream, adapterProcess.StandardOutput.BaseStream); - adapterHost.DispatcherError += (sender, args) => + adapterHost.DispatcherError += (_, args) => { string message = $"DispatcherError - {args.Exception}"; ConsoleWindow.AddMessage(message + "\n", "Adapter", "Error"); @@ -451,7 +451,7 @@ private void UpdateStackFrames() return; } - stackFrames = adapterHost.SendRequestSync(new StackTraceRequest() { ThreadId = MainThread.Id }).StackFrames; + stackFrames = adapterHost.SendRequestSync(new StackTraceRequest { ThreadId = MainThread.Id }).StackFrames; } @@ -474,7 +474,7 @@ private void UpdateVariables() foreach (StackFrame stackFrame in stackFrames) { - List stackScopes = adapterHost.SendRequestSync(new ScopesRequest() { FrameId = stackFrame.Id }).Scopes; + List stackScopes = adapterHost.SendRequestSync(new ScopesRequest { FrameId = stackFrame.Id }).Scopes; Dictionary> stackVariables = stackScopes.ToDictionary(scope => scope, scope => RetrieveNestedVariables(scope.VariablesReference)); threadVariables.Add(stackFrame, stackVariables); } @@ -499,7 +499,7 @@ private List RetrieveNestedVariables(int variablesReference) { return new(); } - return adapterHost.SendRequestSync(new VariablesRequest() { VariablesReference = variablesReference }).Variables; + return adapterHost.SendRequestSync(new VariablesRequest { VariablesReference = variablesReference }).Variables; } /// @@ -512,7 +512,7 @@ private string RetrieveVariableValue(Variable variable) { if (IsRunning && variable.EvaluateName != null) { - EvaluateResponse value = adapterHost.SendRequestSync(new EvaluateRequest() + EvaluateResponse value = adapterHost.SendRequestSync(new EvaluateRequest { Expression = variable.EvaluateName, FrameId = IsRunning ? null : StackFrame.Id diff --git a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionCodePosition.cs b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionCodePosition.cs index afaabbb88a..0e1fa44116 100644 --- a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionCodePosition.cs +++ b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionCodePosition.cs @@ -6,6 +6,7 @@ using SEE.Utils; using System.IO; using System.Linq; +using Cysharp.Threading.Tasks; using StackFrame = Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages.StackFrame; namespace SEE.UI.DebugAdapterProtocol @@ -87,7 +88,7 @@ private void ShowCodePosition(bool makeActive = false, bool scroll = false, floa { codeWindow = Canvas.AddComponent(); codeWindow.Title = Path.GetFileName(lastCodePath); - codeWindow.EnterFromFile(lastCodePath); + codeWindow.EnterFromFileAsync(lastCodePath).Forget(); manager.AddWindow(codeWindow); codeWindow.OnComponentInitialized += Mark; codeWindow.OnComponentInitialized += MakeActive; diff --git a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionEvents.cs b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionEvents.cs index 4760de1f89..c45d0113d2 100644 --- a/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionEvents.cs +++ b/Assets/SEE/UI/DebugAdapterProtocol/DebugAdapterProtocolSessionEvents.cs @@ -25,16 +25,16 @@ private void OnBreakpointsChanged(string path, int line) { actions.Enqueue(() => { - adapterHost.SendRequest(new SetBreakpointsRequest() + adapterHost.SendRequest(new SetBreakpointsRequest { - Source = new Source() { Path = path, Name = path }, + Source = new Source { Path = path, Name = path }, Breakpoints = DebugBreakpointManager.Breakpoints[path].Values.ToList(), }, _ => { }); }); } /// - /// Handles the begining of hovering a word. + /// Handles the beginning of hovering a word. /// /// The code window containing the hovered word. /// The info of the hovered word. @@ -66,11 +66,11 @@ private void UpdateHoverTooltip() return; } - string expression = ((TMP_WordInfo)hoveredWord).GetWord(); + string expression = hoveredWord.Value.GetWord(); try { - EvaluateResponse result = adapterHost.SendRequestSync(new EvaluateRequest() + EvaluateResponse result = adapterHost.SendRequestSync(new EvaluateRequest { Expression = expression, Context = capabilities.SupportsEvaluateForHovers == true ? EvaluateArguments.ContextValue.Hover : null, @@ -133,14 +133,14 @@ private void OnInitializedEvent(InitializedEvent initializedEvent) adapterHost.SendRequest(Adapter.GetLaunchRequest(), _ => IsRunning = true); foreach ((string path, Dictionary breakpoints) in DebugBreakpointManager.Breakpoints) { - adapterHost.SendRequest(new SetBreakpointsRequest() + adapterHost.SendRequest(new SetBreakpointsRequest { - Source = new Source() { Path = path, Name = path }, + Source = new Source { Path = path, Name = path }, Breakpoints = breakpoints.Values.ToList(), }, _ => { }); } - adapterHost.SendRequest(new SetFunctionBreakpointsRequest() { Breakpoints = new() }, _ => { }); - adapterHost.SendRequest(new SetExceptionBreakpointsRequest() { Filters = new() }, _ => { }); + adapterHost.SendRequest(new SetFunctionBreakpointsRequest { Breakpoints = new() }, _ => { }); + adapterHost.SendRequest(new SetExceptionBreakpointsRequest { Filters = new() }, _ => { }); if (capabilities.SupportsConfigurationDoneRequest == true) { adapterHost.SendRequest(new ConfigurationDoneRequest(), _ => { }); @@ -227,7 +227,7 @@ private void OnStoppedEvent(StoppedEvent stoppedEvent) return; } - ExceptionInfoResponse exceptionInfo = adapterHost.SendRequestSync(new ExceptionInfoRequest() + ExceptionInfoResponse exceptionInfo = adapterHost.SendRequestSync(new ExceptionInfoRequest { ThreadId = MainThread.Id, }); @@ -358,7 +358,7 @@ private void OnConsoleInput(string text) /// /// Queues a continue request. /// - void OnContinue() + private void OnContinue() { actions.Enqueue(() => { @@ -373,7 +373,7 @@ void OnContinue() /// /// Queues a pause request. /// - void OnPause() + private void OnPause() { actions.Enqueue(() => { @@ -388,7 +388,7 @@ void OnPause() /// /// Queues a reverse continue request. /// - void OnReverseContinue() + private void OnReverseContinue() { actions.Enqueue(() => { @@ -403,7 +403,7 @@ void OnReverseContinue() /// /// Queues a next request. /// - void OnNext() + private void OnNext() { actions.Enqueue(() => { @@ -418,7 +418,7 @@ void OnNext() /// /// Queues a step back request. /// - void OnStepBack() + private void OnStepBack() { actions.Enqueue(() => { @@ -433,7 +433,7 @@ void OnStepBack() /// /// Queues a step in request. /// - void OnStepIn() + private void OnStepIn() { actions.Enqueue(() => { @@ -448,7 +448,7 @@ void OnStepIn() /// /// Queues a step out request. /// - void OnStepOut() + private void OnStepOut() { actions.Enqueue(() => { @@ -463,7 +463,7 @@ void OnStepOut() /// /// Queues a restart request. /// - void OnRestart() + private void OnRestart() { actions.Enqueue(() => { @@ -474,7 +474,7 @@ void OnRestart() /// /// Queues a terminate request. /// - void OnStop() + private void OnStop() { actions.Enqueue(() => { @@ -487,6 +487,7 @@ void OnStop() Disconnect(); } }); + return; // Tries to stop the debuggee gracefully. void Terminate() diff --git a/Assets/SEE/UI/HelpSystem/HelpSystemBuilder.cs b/Assets/SEE/UI/HelpSystem/HelpSystemBuilder.cs index 74800a05d2..9d1b7f7025 100644 --- a/Assets/SEE/UI/HelpSystem/HelpSystemBuilder.cs +++ b/Assets/SEE/UI/HelpSystem/HelpSystemBuilder.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.IO; using SEE.UI.Menu; +using SEE.Utils; using TMPro; using UnityEngine; using UnityEngine.Video; @@ -107,25 +108,20 @@ private static GameObject HelpSystemObject() } /// - /// The path to the default-icon for an HelpSystemEntry in the nested menu. + /// The path to the default-icon for a in the nested menu. /// - private const string entryIcon = "Materials/ModernUIPack/Eye"; + private const char entryIcon = Icons.Eye; /// - /// The path to the default-icon for an RefEntry in the nested menu. + /// The path to the default-icon for a RefEntry in the nested menu. /// - private const string refIcon = "Materials/ModernUIPack/Plus"; + private const char refIcon = '+'; /// /// The LinkedListEntries of the currently selected HelpSystemEntry. /// public static LinkedList CurrentEntries; - /// - /// The space where the entry is inside. - /// - public static GameObject EntrySpace; - /// /// The headline gameObject of the helpSystemEntry or rather the headline which is inside of the dynamicPanel. /// @@ -150,20 +146,19 @@ public static MenuEntry CreateNewHelpSystemEntry LinkedList keywords, HelpSystemEntry entry = null) { - return new MenuEntry(selectAction: () => { Execute(entry, title, keywords, videoPath); }, - unselectAction: null, - title: title, - description: description, - entryColor: entryColor, - icon: Resources.Load(entryIcon)); + return new MenuEntry(SelectAction: () => Execute(entry, title, keywords, videoPath), + Title: title, + Description: description, + EntryColor: entryColor, + Icon: entryIcon); } /// /// Creates a new Ref-Entry for the HelpSystemMenu. That means, this entry contains a list of further entries, - /// which are opened as the lower hierachy-layer onclick. These entries are only responsible for the structure of the HelpSystemMenu, + /// which are opened as the lower hierarchy-layer onclick. These entries are only responsible for the structure of the HelpSystemMenu, /// they are not executing an HelpSystemEntry. /// - /// The inner Entries, which are displayed onclick as the lower hierachy-layer. + /// The inner Entries, which are displayed onclick as the lower hierarchy-layer. /// The title of the RefEntry. /// The description of the RefEntry, displayed as a tooltip. /// The color of the Ref-Entry. @@ -174,18 +169,18 @@ public static NestedMenuEntry CreateNewRefEntry(List inner title: title, description: description, entryColor: entryColor, - icon: Resources.Load(refIcon)); + icon: refIcon); } /// /// Creates the Main-Menu of the HelpSystemMenu. - /// More specific, it creates the highest Hierachy-Layer, + /// More specific, it creates the highest Hierarchy-Layer, /// where new Layers can be attached to with the functions above. /// /// The title of the HelpSystem-MainMenu. /// The description of the HelpSystem-MainMenu. /// The icon of the HelpSystem-MainMenu. - /// The MenuEntries which are displayed inside of the MainMenu for more hierachy-layers. + /// The MenuEntries which are displayed inside of the MainMenu for more hierarchy-layers. /// The Main-Menu as a NestedMenu. public static NestedListMenu CreateMainMenu(string title, string description, string icon, List mainMenuEntries) { @@ -208,7 +203,7 @@ public static NestedListMenu CreateMainMenu(string title, string description, st /// The title of the HelpSystemEntry. /// The path of the video which should be displayed. /// All instructions which should be displayed and spoken aloud. - public static void Execute(HelpSystemEntry helpSystem, string entryTitle, LinkedList instructions, string videoPath) + private static void Execute(HelpSystemEntry helpSystem, string entryTitle, LinkedList instructions, string videoPath) { helpSystem.EntryShown = true; helpSystem.ShowEntry(); diff --git a/Assets/SEE/UI/HelpSystem/HelpSystemEntry.cs b/Assets/SEE/UI/HelpSystem/HelpSystemEntry.cs index 6802db22e4..ebc3efd8de 100644 --- a/Assets/SEE/UI/HelpSystem/HelpSystemEntry.cs +++ b/Assets/SEE/UI/HelpSystem/HelpSystemEntry.cs @@ -245,7 +245,6 @@ public void ShowEntry() { helpSystemSpace = PrefabInstantiator.InstantiatePrefab(helpSystemEntrySpacePrefab, Canvas.transform, false); helpSystemEntry = PrefabInstantiator.InstantiatePrefab(helpSystemEntryPrefab, helpSystemSpace.transform, false); - HelpSystemBuilder.EntrySpace = helpSystemSpace; helpSystemSpace.transform.localScale = new Vector3(1.7f, 1.7f); RectTransform dynamicPanel = helpSystemSpace.transform.GetChild(2).GetComponent(); dynamicPanel.sizeDelta = new Vector2(550, 425); diff --git a/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs b/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs index 97fb47f99b..17dcad2146 100644 --- a/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs +++ b/Assets/SEE/UI/Menu/Desktop/SimpleListMenuDesktop.cs @@ -1,6 +1,9 @@ -using Michsky.UI.ModernUIPack; +using System.Linq; +using Michsky.UI.ModernUIPack; +using SEE.GO; using SEE.Utils; using Sirenix.Utilities; +using TMPro; using UnityEngine; using UnityEngine.UI; @@ -52,7 +55,7 @@ public partial class SimpleListMenu where T : MenuEntry /// /// The menu entry. /// The game object of the entry. - public GameObject EntryGameObject(T entry) => EntryList.transform.Find(entry.Title)?.gameObject; + protected GameObject EntryGameObject(T entry) => EntryList.transform.Cast().FirstOrDefault(x => x.name == entry.Title)?.gameObject; /// /// Initializes the menu. @@ -110,14 +113,18 @@ protected virtual void AddButton(T entry) // title and icon button.name = entry.Title; - ButtonManagerBasicWithIcon buttonManager = button.GetComponent(); + ButtonManagerBasic buttonManager = button.MustGetComponent(); + TextMeshProUGUI iconText = button.transform.Find("Icon").gameObject.MustGetComponent(); buttonManager.buttonText = entry.Title; - buttonManager.buttonIcon = entry.Icon; + iconText.text = entry.Icon.ToString(); // hover listeners - PointerHelper pointerHelper = button.GetComponent(); - pointerHelper.EnterEvent.AddListener(_ => Tooltip.ActivateWith(entry.Description)); - pointerHelper.ExitEvent.AddListener(_ => Tooltip.Deactivate()); + PointerHelper pointerHelper = button.MustGetComponent(); + if (entry.Description != null) + { + pointerHelper.EnterEvent.AddListener(_ => Tooltip.ActivateWith(entry.Description)); + pointerHelper.ExitEvent.AddListener(_ => Tooltip.Deactivate()); + } // adds clickEvent listener or show that button is disabled if (entry.Enabled) @@ -131,10 +138,10 @@ protected virtual void AddButton(T entry) // colors Color color = entry.Enabled ? entry.EntryColor : entry.DisabledColor; - button.GetComponent().color = color; + button.MustGetComponent().color = color; Color textColor = color.IdealTextColor(); buttonManager.normalText.color = textColor; - buttonManager.normalImage.color = textColor; + iconText.color = textColor; } /// diff --git a/Assets/SEE/UI/Menu/MenuEntry.cs b/Assets/SEE/UI/Menu/MenuEntry.cs index 560321cfa9..4690a82fbd 100644 --- a/Assets/SEE/UI/Menu/MenuEntry.cs +++ b/Assets/SEE/UI/Menu/MenuEntry.cs @@ -1,7 +1,6 @@ using System; using SEE.Utils; using UnityEngine; -using UnityEngine.Events; namespace SEE.UI.Menu { @@ -9,70 +8,21 @@ namespace SEE.UI.Menu /// This class represents a platform-independent entry in a , /// which performs a designated action when it is chosen there. /// - public class MenuEntry + /// The action to take when the entry is selected. + /// The action to take when the entry is unselected. May be null if nothing + /// should happen when it is unselected. + /// The title of the entry. + /// A description of the entry. + /// The color with which this entry shall be displayed. + /// Whether this entry should be enabled (i.e., whether it can be selected). + /// The icon which shall be displayed alongside this entry, + /// given as a FontAwesome codepoint. See for more information. + public record MenuEntry(Action SelectAction, string Title, Action UnselectAction = null, string Description = null, + Color EntryColor = default, bool Enabled = true, char Icon = ' ') { - /// - /// The title of this entry. - /// - public readonly string Title; - - /// - /// A description of this entry. - /// - public readonly string Description; - - /// - /// The color of this entry. - /// - public readonly Color EntryColor; - - /// - /// An icon for this entry. - /// - public readonly Sprite Icon; - - /// - /// The action to be taken when the entry is selected. - /// - public readonly UnityAction SelectAction; - - /// - /// The action to be taken when the entry is deselected. - /// - public readonly UnityAction UnselectAction; - - /// - /// Whether this entry is currently enabled (i.e. whether it can be selected.) - /// Defaults to true. - /// - public bool Enabled; - /// /// The color of this entry when disabled. /// public Color DisabledColor => EntryColor.WithAlpha(0.2f); - - /// - /// Instantiates and returns a new MenuEntry. - /// - /// What action to take when the entry is selected. - /// What action to take when the entry is unselected. - /// The title of the entry. - /// A description of the entry. - /// The color with which this entry shall be displayed. - /// Whether this entry should be enabled on creation. - /// The icon which shall be displayed alongside this entry. - /// If is null. - public MenuEntry(UnityAction selectAction, UnityAction unselectAction, string title, string description = null, Color entryColor = default, - bool enabled = true, Sprite icon = null) - { - SelectAction = selectAction; - UnselectAction = unselectAction ?? (() => { }); - Title = title ?? throw new ArgumentNullException(nameof(title)); - Description = description; - EntryColor = entryColor; - Enabled = enabled; - Icon = icon; - } } } diff --git a/Assets/SEE/UI/Menu/NestedMenu.cs b/Assets/SEE/UI/Menu/NestedMenu.cs index 1ee772b8f5..bb253abc20 100644 --- a/Assets/SEE/UI/Menu/NestedMenu.cs +++ b/Assets/SEE/UI/Menu/NestedMenu.cs @@ -132,7 +132,7 @@ private void DescendLevel(NestedMenuEntry nestedEntry, bool withBreadcrumb = // as the title is technically the last element in the breadcrumb) string breadcrumb = withBreadcrumb ? GetBreadcrumb() : string.Empty; Description = nestedEntry.Description + (breadcrumb.Length > 0 ? $"\n{GetBreadcrumb()}" : ""); - Icon = nestedEntry.Icon; + Icon = nestedEntry.MenuIconSprite; nestedEntry.InnerEntries.ForEach(AddEntry); KeywordListener.Unregister(HandleKeyword); KeywordListener.Register(HandleKeyword); @@ -311,8 +311,7 @@ private async UniTaskVoid SearchTextEnteredAsync() .Select(x => allEntries[x.Value]) .ToList(); - NestedMenuEntry resultEntry = new(results, Title, Description, - default, default, Icon); + NestedMenuEntry resultEntry = new(results, Title, Description, menuIconSprite: Icon); DescendLevel(resultEntry, withBreadcrumb: false); } finally diff --git a/Assets/SEE/UI/Menu/NestedMenuEntry.cs b/Assets/SEE/UI/Menu/NestedMenuEntry.cs index 70eae1fe08..ce96157c5f 100644 --- a/Assets/SEE/UI/Menu/NestedMenuEntry.cs +++ b/Assets/SEE/UI/Menu/NestedMenuEntry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SEE.Utils; using UnityEngine; namespace SEE.UI.Menu @@ -9,13 +10,18 @@ namespace SEE.UI.Menu /// A button which opens another menu when clicking on it. /// Must be used inside a . /// - public class NestedMenuEntry : MenuEntry where T : MenuEntry + public record NestedMenuEntry : MenuEntry where T : MenuEntry { /// /// The menu entries which shall fill the menu when selecting this entry. /// public readonly List InnerEntries; + /// + /// The sprite of the icon of the menu itself. + /// + public readonly Sprite MenuIconSprite; + /// /// Instantiates and returns a new . /// @@ -24,13 +30,16 @@ public class NestedMenuEntry : MenuEntry where T : MenuEntry /// A description of the entry. /// The color with which this entry shall be displayed. /// Whether this entry should be enabled on creation. - /// The icon which shall be displayed alongside this entry. + /// The FontAwesome icon which shall be displayed alongside this entry. + /// The sprite of the icon of the menu itself. /// If is null. public NestedMenuEntry(IEnumerable innerEntries, string title, string description = null, - Color entryColor = default, bool enabled = true, Sprite icon = null) : - base(() => { }, () => { }, title, description, entryColor, enabled, icon) + Color entryColor = default, bool enabled = true, + char icon = Icons.Bars, Sprite menuIconSprite = null) + : base(() => { }, title, () => { }, description, entryColor, enabled, icon) { InnerEntries = innerEntries?.ToList() ?? throw new ArgumentNullException(nameof(innerEntries)); + MenuIconSprite = menuIconSprite; } } } diff --git a/Assets/SEE/UI/Menu/SelectionMenu.cs b/Assets/SEE/UI/Menu/SelectionMenu.cs index a269161a8b..a50f51c5db 100644 --- a/Assets/SEE/UI/Menu/SelectionMenu.cs +++ b/Assets/SEE/UI/Menu/SelectionMenu.cs @@ -34,7 +34,7 @@ public T ActiveEntry activeEntry = value; if (oldActiveEntry != null) { - oldActiveEntry.UnselectAction(); + oldActiveEntry.UnselectAction?.Invoke(); OnEntryUnselected?.Invoke(oldActiveEntry); } OnActiveEntryChanged?.Invoke(); diff --git a/Assets/SEE/UI/Menu/SimpleListMenu.cs b/Assets/SEE/UI/Menu/SimpleListMenu.cs index f8a8a4ec4a..5a0122caa4 100644 --- a/Assets/SEE/UI/Menu/SimpleListMenu.cs +++ b/Assets/SEE/UI/Menu/SimpleListMenu.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using UnityEngine.Events; using UnityEngine.Windows.Speech; namespace SEE.UI.Menu @@ -98,6 +98,11 @@ public void RemoveEntry(T entry) OnEntryRemoved?.Invoke(entry); } + /// + /// Removes all menu entries. + /// + public void ClearEntries() => entries.ToList().ForEach(RemoveEntry); + /// /// Selects a menu entry. /// It is assumed that the menu contains the entry. @@ -145,26 +150,26 @@ protected override void HandleKeyword(PhraseRecognizedEventArgs args) /// /// Triggers when was changed. /// - public event UnityAction OnAllowNoSelectionChanged; + public event Action OnAllowNoSelectionChanged; /// /// Triggers when was changed. /// - public event UnityAction OnHideAfterSelectionChanged; + public event Action OnHideAfterSelectionChanged; /// /// Triggers when an entry was added. () /// - public event UnityAction OnEntryAdded; + public event Action OnEntryAdded; /// /// Triggers when an entry was removed. () /// - public event UnityAction OnEntryRemoved; + public event Action OnEntryRemoved; /// /// Triggers when an entry was selected. () /// - public event UnityAction OnEntrySelected; + public event Action OnEntrySelected; } } diff --git a/Assets/SEE/UI/OpeningDialog.cs b/Assets/SEE/UI/OpeningDialog.cs index 56b8f27cfb..1fb481eabc 100644 --- a/Assets/SEE/UI/OpeningDialog.cs +++ b/Assets/SEE/UI/OpeningDialog.cs @@ -53,34 +53,30 @@ private IList SelectionEntries() return new List { - new(selectAction: StartHost, - unselectAction: null, - title: "Host", - description: "Starts a server and local client process.", - entryColor: NextColor(), - icon: Resources.Load("Icons/Host")), + new(SelectAction: StartHost, + Title: "Host", + Description: "Starts a server and local client process.", + EntryColor: NextColor(), + Icon: Icons.Broadcast), - new(selectAction: StartClient, - unselectAction: null, - title: "Client", - description: "Starts a local client connection to a server.", - entryColor: NextColor(), - icon: Resources.Load("Icons/Client")), + new(SelectAction: StartClient, + Title: "Client", + Description: "Starts a local client connection to a server.", + EntryColor: NextColor(), + Icon: Icons.Link), #if ENABLE_VR - new(selectAction: ToggleEnvironment, - unselectAction: null, - title: "Toggle Desktop/VR", - description: "Toggles between desktop and VR hardware.", - entryColor: NextColor(), - icon: Resources.Load("Icons/Client")), + new(SelectAction: ToggleEnvironment, + Title: "Toggle Desktop/VR", + Description: "Toggles between desktop and VR hardware.", + EntryColor: NextColor(), + Icon: Icons.VR), #endif - new(selectAction: Settings, - unselectAction: null, - title: "Settings", - description: "Allows to set additional network settings.", - entryColor: Color.gray, - icon: Resources.Load("Icons/Settings")), + new(SelectAction: Settings, + Title: "Settings", + Description: "Allows to set additional network settings.", + EntryColor: Color.gray, + Icon: Icons.Gear) }; Color NextColor() diff --git a/Assets/SEE/UI/PropertyDialog/ButtonProperty.cs b/Assets/SEE/UI/PropertyDialog/ButtonProperty.cs index 96b77785ab..374fc2bb4d 100644 --- a/Assets/SEE/UI/PropertyDialog/ButtonProperty.cs +++ b/Assets/SEE/UI/PropertyDialog/ButtonProperty.cs @@ -10,7 +10,7 @@ namespace SEE.UI.PropertyDialog { /// - /// A button for a for a property dialog. + /// A button for a property dialog. /// public class ButtonProperty : Property { @@ -30,9 +30,9 @@ public class ButtonProperty : Property private GameObject button; /// - /// Used to store the icon of the button. + /// The codepoint of the icon for the button. /// - public Sprite IconSprite; + public char Icon; /// /// Saves which method of the hide action is to be executed. @@ -80,6 +80,7 @@ protected override void StartDesktop() SetupTooltip(); SetUpButton(); + return; void SetUpButton() { @@ -87,10 +88,10 @@ void SetUpButton() GameObject text = button.transform.Find("Text").gameObject; GameObject icon = button.transform.Find("Icon").gameObject; - if (!button.TryGetComponentOrLog(out ButtonManagerBasicWithIcon buttonManager) + if (!button.TryGetComponentOrLog(out ButtonManagerBasic buttonManager) || !button.TryGetComponentOrLog(out Image buttonImage) || !text.TryGetComponentOrLog(out TextMeshProUGUI textMeshPro) - || !icon.TryGetComponentOrLog(out Image iconImage) + || !icon.TryGetComponentOrLog(out TextMeshProUGUI iconText) || !button.TryGetComponentOrLog(out PointerHelper pointerHelper)) { return; @@ -99,8 +100,8 @@ void SetUpButton() textMeshPro.fontSize = 20; buttonImage.color = ButtonColor; textMeshPro.color = ButtonColor.IdealTextColor(); - iconImage.color = ButtonColor.IdealTextColor(); - iconImage.sprite = IconSprite; + iconText.color = ButtonColor.IdealTextColor(); + iconText.text = Icon.ToString(); buttonManager.buttonText = Name; buttonManager.clickEvent.AddListener(Clicked); diff --git a/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs b/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs index 4d2964f799..a6737ad38f 100644 --- a/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs +++ b/Assets/SEE/UI/RuntimeConfigMenu/RuntimeTabMenu.cs @@ -263,7 +263,7 @@ string GetButtonGroup(MemberInfo memberInfo) => (memberInfo.GetCustomAttributes().OfType().FirstOrDefault() ?? new RuntimeButtonAttribute(null, null)).Name; - // ordered depending if a setting is primitive or has nested settings + // ordered depending on whether a setting is primitive or has nested settings bool SortIsNotNested(MemberInfo memberInfo) { object value; @@ -388,12 +388,11 @@ private GameObject CreateOrGetViewGameObject(IEnumerable attributes) if (entry == null) { entry = new MenuEntry( - () => { }, - () => { }, - tabName, - $"Settings for {tabName}", - GetColorForTab(), - Resources.Load("Materials/Charts/MoveIcon") + SelectAction: () => { }, + Title: tabName, + Description: $"Settings for {tabName}", + EntryColor: GetColorForTab(), + Icon: Icons.Move ); AddEntry(entry); } diff --git a/Assets/SEE/UI/Tooltip.cs b/Assets/SEE/UI/Tooltip.cs index 7da0438271..8bb293d948 100644 --- a/Assets/SEE/UI/Tooltip.cs +++ b/Assets/SEE/UI/Tooltip.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using DG.Tweening; using Michsky.UI.ModernUIPack; @@ -221,6 +222,12 @@ public static void ActivateWith(string text, AfterShownBehavior afterShownBehavi Instance.ChangeText(text, afterShownBehavior); } + /// + /// Whether the tooltip is currently active. + /// Note that "active" does not necessarily mean that the tooltip is currently visible. + /// + public static bool IsActivated => Instance.text != null; + /// /// Will hide the tooltip by fading it out if it's currently visible. /// If has been called prior to this and is active, it will be halted. @@ -286,8 +293,7 @@ protected override void StartDesktop() tooltipManager.allowUpdating = true; // Move tooltip to front of layer hierarchy tooltipManager.gameObject.transform.SetAsLastSibling(); - // tooltipObject only has 1 child, and will never have more than that - if (tooltipManager.tooltipObject.transform.GetChild(0).gameObject.TryGetComponentOrLog(out canvasGroup)) + if (tooltipManager.tooltipObject.transform.Find("Anchor/Content").gameObject.TryGetComponentOrLog(out canvasGroup)) { // Get the actual text object TextMeshProUGUI[] texts = tooltipManager.tooltipContent.GetComponentsInChildren(); diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs index 3c65aa0b09..8543c927c2 100644 --- a/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindow.cs @@ -1,6 +1,9 @@ using System; using System.Linq; +using Cysharp.Threading.Tasks; using DG.Tweening; +using MoreLinq; +using SEE.Tools.LSP; using SEE.Utils; using TMPro; using UnityEngine; @@ -52,6 +55,19 @@ public partial class CodeWindow : BaseWindow /// private int lines; + /// + /// The LSP handler for this code window. + /// + /// Will only be set if the LSP feature is enabled and active for this code window. + /// + private LSPHandler lspHandler; + + /// + /// The handler for the context menu of this code window, which provides various navigation options, + /// such as "Go to Definition" or "Find References". + /// + private ContextMenuHandler contextMenu; + /// /// Path to the code window content prefab. /// @@ -88,26 +104,34 @@ public partial class CodeWindow : BaseWindow private static TMP_WordInfo? lastHoveredWord; /// - /// Visually marks the line at the given and scrolls to it. - /// Will also unmark any other line. Sets to - /// . + /// Whether the code window contains text. + /// + public bool ContainsText => text != null; + + /// + /// Visually highlights the line number at the given and scrolls to it. + /// Will also unhighlight any other line. Sets to . /// Clears the markers for line numbers smaller than 1. /// - /// The line number of the line to mark and scroll to (1-indexed) + /// The line number of the line to highlight and scroll to (1-indexed) public void MarkLine(int lineNumber) { + const string markColor = ""; + int markColorLength = markColor.Length; markedLine = lineNumber; - string[] allLines = textMesh.text.Split('\n').Select(x => x.EndsWith("") ? x.Substring(16, x.Length - 16 - 7) : x).ToArray(); + string[] allLines = textMesh.text.Split('\n') + .Select(x => x.StartsWith(markColor) ? $"{x[markColorLength..]}" : x) + .ToArray(); if (lineNumber < 1) { - textMesh.text = string.Join("\n", allLines); + text = string.Join("\n", allLines); } else { - string markLine = $"{allLines[lineNumber - 1]}"; - textMesh.text = string.Join("\n", allLines.Take(lineNumber - 1).Append(markLine).Concat(allLines.Skip(lineNumber).Take(lines - lineNumber + 1))); + string markLine = $"{markColor}{allLines[lineNumber - 1][markColorLength..]}"; + text = string.Join("\n", allLines.Exclude(lineNumber - 1, 1).Insert(new[] { markLine }, lineNumber - 1)); } - + textMesh.text = text; } #region Visible Line Calculation @@ -166,13 +190,7 @@ public int ScrolledVisibleLine DOTween.Sequence().Append(DOTween.To(() => ImmediateVisibleLine, f => ImmediateVisibleLine = f, value - 1, 1f)) .AppendCallback(() => scrollingTo = 0); - // FIXME (#250): TMP bug: Large files cause issues with highlighting text. This is just a workaround. - // See https://github.com/uni-bremen-agst/SEE/issues/250#issuecomment-819653373 - if (text.Length < 16382) - { - MarkLine(value); - } - + MarkLine(value); ScrollEvent.Invoke(); } } @@ -201,7 +219,8 @@ private float ImmediateVisibleLine } else { - scrollRect.verticalNormalizedPosition = 1 - value / (lines - 1 - excessLines); + scrollRect.verticalNormalizedPosition = 1 - (value-1) / (lines - 1 - excessLines); + scrollRect.horizontalNormalizedPosition = 0; } } } @@ -225,17 +244,17 @@ protected override void InitializeFromValueObject(WindowValues valueObject) if (codeValues.Path != null) { - EnterFromFile(codeValues.Path); + EnterFromFileAsync(codeValues.Path).ContinueWith(() => ScrolledVisibleLine = codeValues.VisibleLine).Forget(); } else if (codeValues.Text != null) { EnterFromText(codeValues.Text.Split('\n')); + ScrolledVisibleLine = codeValues.VisibleLine; } else { throw new ArgumentException("Invalid value object. Either FilePath or Text must not be null."); } - ScrolledVisibleLine = codeValues.VisibleLine; } public override void UpdateFromNetworkValueObject(WindowValues valueObject) diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs new file mode 100644 index 0000000000..c48e49a9ce --- /dev/null +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs @@ -0,0 +1,314 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Cysharp.Threading.Tasks; +using Cysharp.Threading.Tasks.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using SEE.Controls; +using SEE.Controls.Actions; +using SEE.Tools.LSP; +using SEE.UI.Menu; +using SEE.UI.Notification; +using SEE.UI.PopupMenu; +using SEE.Utils; +using UnityEngine; +using UnityEngine.Assertions; +using Range = SEE.DataModel.DG.Range; + +namespace SEE.UI.Window.CodeWindow +{ + /// + /// Partial class containing methods related to context menus and LSP navigation in code windows. + /// In addition, this part contains a record representing a context menu handler for a code window. + /// + public partial class CodeWindow + { + /// + /// A handler for the context menu of code windows that provides various navigation options, + /// such as "Go to Definition" or "Find References". + /// + /// The path of the file that is displayed in the code window. + /// The LSP handler that provides the language server capabilities. + /// A list menu that's shown when the user needs to make some selection, + /// such as when choosing a reference to navigate to. + /// The context menu that this class manages. + /// A callback that opens the given URI and range in a new code window + /// or scrolls to the range if the URI is the same as the current file. + private record ContextMenuHandler(string path, LSPHandler lspHandler, PopupMenu.PopupMenu contextMenu, + SimpleListMenu simpleListMenu, Action OpenSelection) + { + /// + /// Creates and initializes a new context menu handler for the given . + /// + /// The code window for which the context menu should be created. + /// The created context menu handler. + public static ContextMenuHandler FromCodeWindow(CodeWindow codeWindow) + { + PopupMenu.PopupMenu contextMenu = codeWindow.gameObject.AddComponent(); + SimpleListMenu simpleListMenu = codeWindow.gameObject.AddComponent(); + return new ContextMenuHandler(codeWindow.FilePath, codeWindow.lspHandler, contextMenu, + simpleListMenu, codeWindow.OpenSelection); + } + + /// + /// Shows the context menu at the given , assuming the user right-clicked + /// at the given and . + /// + /// The 0-indexed line where the user right-clicked. + /// The 0-indexed column where the user right-clicked. + /// The position where the context menu should be shown. + /// The word at the given and . + public void Show(int line, int column, Vector2 position, string contextWord) + { + IList actions = new List(); + + if (lspHandler.ServerCapabilities.ReferencesProvider != null) + { + actions.Add(new("Find References", WithLineColumn(ShowReferences), Icons.MagnifyingGlass)); + } + if (lspHandler.ServerCapabilities.DeclarationProvider != null) + { + actions.Add(new("Go to Declaration", WithLineColumn(ShowDeclaration), Icons.IncomingEdge)); + } + if (lspHandler.ServerCapabilities.DefinitionProvider != null) + { + actions.Add(new("Go to Definition", WithLineColumn(ShowDefinition), Icons.IncomingEdge)); + } + if (lspHandler.ServerCapabilities.TypeDefinitionProvider != null) + { + actions.Add(new("Go to Type Definition", WithLineColumn(ShowTypeDefinition), Icons.IncomingEdge)); + } + if (lspHandler.ServerCapabilities.ImplementationProvider != null) + { + actions.Add(new("Go to Implementation", WithLineColumn(ShowImplementation), Icons.OutgoingEdge)); + } + if (lspHandler.ServerCapabilities.CallHierarchyProvider != null) + { + actions.Add(new("Show Outgoing Calls", WithLineColumn(ShowOutgoingCalls), Icons.Sitemap)); + } + if (lspHandler.ServerCapabilities.TypeHierarchyProvider != null) + { + actions.Add(new("Show Supertypes", WithLineColumn(ShowSupertypes), Icons.Sitemap)); + } + + if (actions.Count > 0) + { + contextMenu.ShowWith(actions, position); + } + return; + + // Calls the given action with the line, column, and context word given to Show. + Action WithLineColumn(Action action) => () => action(line, column, contextWord); + } + + /// + /// Shows the outgoing calls for the given and . + /// + /// The 0-indexed line for which to show the outgoing calls. + /// The 0-indexed column for which to show the outgoing calls. + /// The word at the given and . + private void ShowOutgoingCalls(int line, int column, string contextWord) + { + const string name = "Outgoing Calls"; + MenuEntriesForLocationsAsync(lspHandler.OutgoingCalls(_ => true, path, line, column) + .Select(x => (x.Uri.ToUri(), Range.FromLspRange(x.Range), x.Name)), + name, contextWord) + .ContinueWith(entries => ShowEntries(entries, name, contextWord)).Forget(); + } + + /// + /// Shows the outgoing calls for the given and . + /// + /// The 0-indexed line for which to show the outgoing calls. + /// The 0-indexed column for which to show the outgoing calls. + /// The word at the given and . + private void ShowSupertypes(int line, int column, string contextWord) + { + const string name = "Supertypes"; + MenuEntriesForLocationsAsync(lspHandler.Supertypes(_ => true, path, line, column) + .Select(x => (x.Uri.ToUri(), Range.FromLspRange(x.Range), x.Name)), + name, contextWord) + .ContinueWith(entries => ShowEntries(entries, name, contextWord)).Forget(); + } + + /// + /// Shows the references for the given and . + /// + /// The 0-indexed line for which to show the references. + /// The 0-indexed column for which to show the references. + /// The word at the given and . + private void ShowReferences(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.References(path, line, column, includeDeclaration: true), "References", contextWord).Forget(); + + /// + /// Shows the declaration for the given and . + /// + /// The 0-indexed line for which to show the declaration. + /// The 0-indexed column for which to show the declaration. + /// The word at the given and . + private void ShowDeclaration(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.Declaration(path, line, column), "Declarations", contextWord).Forget(); + + /// + /// Shows the definition for the given and . + /// + /// The 0-indexed line for which to show the definition. + /// The 0-indexed column for which to show the definition. + /// The word at the given and . + public void ShowDefinition(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.Definition(path, line, column), "Definitions", contextWord).Forget(); + + /// + /// Shows the implementation for the given and . + /// + /// The 0-indexed line for which to show the implementation. + /// The 0-indexed column for which to show the implementation. + /// The word at the given and . + private void ShowImplementation(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.Implementation(path, line, column), "Implementations", contextWord).Forget(); + + /// + /// Shows the type definition for the given and . + /// + /// The 0-indexed line for which to show the type definition. + /// The 0-indexed column for which to show the type definition. + /// The word at the given and . + private void ShowTypeDefinition(int line, int column, string contextWord) => + ShowLocationsAsync(lspHandler.TypeDefinition(path, line, column), "Type Definitions", contextWord).Forget(); + + /// + /// Opens a menu for the given with the given , + /// letting the user choose one of the locations to navigate to. + /// + /// The locations to show in the menu. + /// The name of the locations, e.g. "Definitions". + /// The word for which the locations are shown. + private async UniTask ShowLocationsAsync(IUniTaskAsyncEnumerable locations, string name, string contextWord) + { + IList entries = await MenuEntriesForLocationsAsync(locations.Select(DeconstructLocation), name, contextWord); + ShowEntries(entries, name, contextWord); + } + + /// + /// Deconstructs the given into a URI, range, and title. + /// + /// The location to deconstruct. + /// A tuple containing the URI, range, and title of the location. + private static (Uri, Range, string) DeconstructLocation(LocationOrLocationLink location) + { + Range targetRange; + Uri targetUri; + if (location.IsLocation) + { + Location loc = location.Location!; + targetRange = Range.FromLspRange(loc.Range); + targetUri = loc.Uri.ToUri(); + } + else + { + LocationLink locLink = location.LocationLink!; + targetRange = Range.FromLspRange(locLink.TargetRange); + targetUri = locLink.TargetUri.ToUri(); + } + return (targetUri, targetRange, null); + } + + /// + /// Generates menu entries for the given . + /// Clicking on an entry will invoke . + /// + /// The locations to generate menu entries for. + /// The name of the locations, e.g. "Definitions". + /// The word for which the locations are shown. + /// The generated menu entries. + private async UniTask> MenuEntriesForLocationsAsync(IUniTaskAsyncEnumerable<(Uri, Range, string)> locations, string name, string contextWord) + { + IList entries = new List(); + using (LoadingSpinner.ShowIndeterminate($"Loading {name} for \"{contextWord}\"...")) + { + await foreach ((Uri targetUri, Range targetRange, string title) in locations) + { + Uri uri = targetUri; + if (lspHandler.ProjectUri?.IsBaseOf(uri) ?? false) + { + // Truncate path above the project's base path to make the result more readable. + uri = lspHandler.ProjectUri.MakeRelativeUri(uri); + } + entries.Add(new(SelectAction: () => OpenSelection(targetUri, targetRange), + Title: title ?? $"{uri}: {targetRange}", + Icon: Icons.Crosshairs, + EntryColor: new Color(0.051f, 0.3608f, 0.1333f))); + } + await UniTask.SwitchToMainThread(); + } + return entries; + } + + /// + /// Opens a menu with the given . + /// If there are no entries, a notification is shown informing the user that there are no results. + /// If there is only one entry, it is directly opened. + /// + /// The entries to show in the menu. + /// The name of the entries, e.g. "Definitions". + /// The word for which the entries are shown. + private void ShowEntries(IList entries, string name, string contextWord) + { + switch (entries.Count) + { + case 0: + ShowNotification.Info("No results", $"No {name} found for \"{contextWord}\".", log: false); + break; + case 1: + // We can directly open the only result. + entries.First().SelectAction(); + break; + default: + // The user needs to select one of the results. + simpleListMenu.ClearEntries(); + simpleListMenu.AddEntries(entries); + simpleListMenu.Title = name; + simpleListMenu.Description = $"Listing {name.ToLower()} for {contextWord}."; + simpleListMenu.Icon = Resources.Load("Materials/Notification/info"); + simpleListMenu.ShowMenu = true; + break; + } + } + } + + /// + /// Opens the selection at the given and . + /// If the URI is the same as the current file, the code window is scrolled to the range, + /// otherwise a new code window is opened. + /// + /// The URI of the file to open. + /// The range to scroll to or show in the new code window. + private void OpenSelection(Uri uri, Range range) + { + // When we're going somewhere else, we should deactivate the current tooltip first. + Tooltip.Deactivate(); + if (FilePath == uri.LocalPath) + { + // If this is the current file, we can just scroll to the range. + ScrolledVisibleLine = range.Start.Line; + } + else + { + // Otherwise, we need to open a different code window. + Assert.IsNotNull(AssociatedGraph); + CodeWindow window = ShowCodeAction.ShowCodeForPath(AssociatedGraph, uri.LocalPath, range, + w => w.ScrolledVisibleLine = range.Start.Line); + if (window != null) + { + WindowSpace manager = WindowSpaceManager.ManagerInstance[WindowSpaceManager.LocalPlayer]; + if (!manager.Windows.Contains(window)) + { + manager.AddWindow(window); + } + manager.ActiveWindow = window; + } + } + } + + } +} diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs.meta b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs.meta new file mode 100644 index 0000000000..c1958dc55e --- /dev/null +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowContextMenu.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 705487168bd5437592d4f98269826340 +timeCreated: 1721735361 \ No newline at end of file diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs new file mode 100644 index 0000000000..45933e523d --- /dev/null +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs @@ -0,0 +1,579 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Cysharp.Threading.Tasks; +using SEE.DataModel.DG; +using SEE.Game; +using SEE.Game.City; +using SEE.GO; +using SEE.UI.Notification; +using SEE.Net.Dashboard; +using SEE.Net.Dashboard.Model.Issues; +using SEE.Scanner; +using SEE.Scanner.Antlr; +using SEE.Scanner.LSP; +using SEE.Tools.LSP; +using SEE.Utils; +using UnityEngine; +using UnityEngine.Assertions; + +namespace SEE.UI.Window.CodeWindow +{ + /// + /// Partial class containing methods related to processing input for the code windows. + /// + public partial class CodeWindow + { + /// + /// The needed padding for the line numbers. + /// + private int neededPadding; + + /// + /// A dictionary mapping each link ID to its issues. + /// + private readonly Dictionary> issueDictionary = new(); + + /// + /// Counter which represents the lowest unfilled position in the . + /// Any index above it must not be filled either. + /// + private char linkCounter = char.MinValue; + + /// + /// List of tokens for this code window. + /// + private List tokenList; + + /// + /// A list of starting offsets of each line in the code window (without rich tags), sorted in ascending order. + /// + /// In other words, this contains the character indices of newlines in the rendered text (the text without + /// the rich tags in them, but with the line numbers at the beginning of each line). + /// + private List CodeWindowOffsets { get; } = new(); + + /// + /// The graph associated with the code city this code window is in. + /// + private Graph AssociatedGraph; + + /// + /// Characters representing newlines. + /// Note that newlines may also consist of aggregations of this set (e.g. "\r\n"). + /// + private static readonly char[] newlineCharacters = { '\r', '\n' }; + + /// + /// Populates the code window with the content of the given token stream. + /// + /// Stream of tokens representing the source code of this code window. + /// Issues for this file. If null, will be automatically retrieved. + /// Entities spanning multiple lines (i.e. using endLine) are not supported. + /// If you wish to use such issues, split the entities up into one per line (see ). + /// + /// If is null. + private void EnterFromTokens(IEnumerable tokens, + IDictionary> issues = null) + { + if (tokens == null) + { + throw new ArgumentNullException(nameof(tokens)); + } + + // Avoid multiple enumeration in case iteration over the data source is expensive. + tokenList = tokens.ToList(); + if (!tokenList.Any()) + { + text = "This file is empty."; + return; + } + + // Unsurprisingly, each newline token corresponds to a new line. + // However, we need to also add "hidden" newlines contained in other tokens, e.g. block comments. + int assumedLines = tokenList.Count(x => x.TokenType.Equals(TokenType.Newline)) + + tokenList.Where(x => !x.TokenType.Equals(TokenType.Newline)) + .Aggregate(0, (_, token) => token.Text.Count(x => x == '\n')); + // Needed padding is the number of lines, because the line number will be at most this long. + neededPadding = Mathf.FloorToInt(Mathf.Log10(assumedLines)) + 1; + text = $"{string.Join("", Enumerable.Repeat(" ", neededPadding - 1))}1 "; + + CodeWindowOffsets.Clear(); + // The first line starts at the beginning of the text after the line number. + CodeWindowOffsets.Add(0); + + // Line number we'll write down next + int lineNumber = 2; + // Offset of the current character in the text (excluding rich tags) + int characterOffset = neededPadding + 1; // + 1 for the space after the line number + // The issue that we're currently marking, if any. + IDisplayableIssue currentlyMarking = null; + // We need reference equality here. + Dictionary> issueTokens = new(ReferenceEqualityComparer.Instance); + + foreach (SEEToken token in tokenList) + { + if (token.TokenType == TokenType.Newline) + { + AppendNewline(ref lineNumber, ref text, token); + } + else if (token.TokenType != TokenType.EOF) // Skip EOF token completely. + { + HandleToken(token); + } + } + + // End any issue marking that may still be open. + EndIssueSegment(); + + // Lines are equal to number of newlines, including the initial newline. + lines = text.Count(x => x.Equals('\n')); // No more weird CRLF shenanigans are present at this point. + text = text.TrimStart('\n'); // Remove leading newline. + + # region Local Functions + + // Appends a newline to the text, assuming we're at theLineNumber and need the given padding. + // Note that newlines MUST be added in this method, not anywhere else, else issue highlighting will break! + void AppendNewline(ref int theLineNumber, ref string text, SEEToken token) + { + // Close an issue marking here if necessary + EndIssueSegment(); + + // First, of course, the newline. + text += "\n"; + // At this point, we need to remember the offset of this new line. + Assert.AreEqual(CodeWindowOffsets.Count, theLineNumber-1); + // + 1 for the newline + CodeWindowOffsets.Add(++characterOffset); + // Add whitespace next to line number, so it's consistent. + int padding = neededPadding - (Mathf.FloorToInt(Mathf.Log10(theLineNumber)) + 1); + // Line number will be typeset in grey to distinguish it from the rest. + text += $"{string.Join(string.Empty, Enumerable.Repeat(' ', padding))}{theLineNumber} "; + characterOffset += neededPadding + 1; + + if (issues?.ContainsKey(theLineNumber) ?? false) + { + HandleIssuesInLine(theLineNumber, token); + } + + theLineNumber++; + } + + // Handles a token which may contain newlines and adds its syntax-highlighted content to the code window. + void HandleToken(SEEToken token) + { + string[] newlineStrings = newlineCharacters.Select(x => x.ToString()).Concat(new[] + { + // Apart from the characters themselves, we also want to look for the concatenation of them + newlineCharacters.Aggregate("", (s, c) => s + c), + newlineCharacters.Aggregate("", (s, c) => c + s) + }).ToArray(); + string[] tokenLines = token.Text.Split(newlineStrings, StringSplitOptions.None); + bool firstRun = true; + foreach (string line in tokenLines) + { + // Any entry after the first is on a separate line. + if (!firstRun) + { + AppendNewline(ref lineNumber, ref text, token); + } + + // Mark any potential issue + if (issueTokens.ContainsKey(token) && issueTokens[token].Count > 0) + { + if (currentlyMarking != null) + { + // We're already marking something. + Assert.IsNotNull(issueDictionary[linkCounter], "Entry must exist when we are currently marking!"); + if (issueTokens[token].Intersect(issueDictionary[linkCounter]).Any()) + { + // If this token contains the same issue, we just need to add any new issues to the current segment. + issueDictionary[linkCounter].UnionWith(issueTokens[token]); + } + else + { + // If it doesn't, we close the current segment and start a new one. + EndIssueSegment(); + StartIssueSegment(token); + } + } + else + { + // We're not marking anything, so we can start a new segment. + StartIssueSegment(token); + } + } + else + { + // No issue is being marked. We should stop marking if we were marking something. + EndIssueSegment(); + } + + if (token.TokenType == TokenType.Whitespace) + { + // We just copy the whitespace verbatim, no need to even color it. + // Note: We have to assume that whitespace will not interfere with TMP's XML syntax. + string replaced = line.Replace("\t", new string(' ', token.Language.TabWidth)); + text += replaced; + characterOffset += replaced.Length; + } + else + { + List tags = token.Modifiers.AsEnumerable().Select(x => x.ToRichTextTag()) + .Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); + foreach (string textTag in tags) + { + text += $"<{textTag}>"; + } + if (currentlyMarking is not { HasColorTags: true }) + { + text += $""; + } + string replaced = line.Replace("", @"<\noparse>"); + text += $"{replaced}"; + characterOffset += replaced.Length; + if (currentlyMarking is not { HasColorTags: true }) + { + text += ""; + } + tags.Reverse(); + foreach (string textTag in tags) + { + text += $""; + } + } + + firstRun = false; + } + } + + // Begins marking a new issue segment starting with the given token. + void StartIssueSegment(SEEToken token) + { + Assert.IsNull(currentlyMarking, "We must not start a new marking segment while we're already marking!"); + IncreaseLinkCounter(); + issueDictionary[linkCounter] = issueTokens[token].ToHashSet(); + currentlyMarking = issueTokens[token].First(); + text += $"{currentlyMarking.OpeningRichTags}"; + } + + // Ends marking the current issue segment, if there is any. + void EndIssueSegment() + { + if (currentlyMarking != null) + { + text += $"{currentlyMarking.ClosingRichTags}"; + } + currentlyMarking = null; + } + + // Prepares issueTokens for the line number, assuming the current token is the newline token + // delineating the beginning of this line. + void HandleIssuesInLine(int theLineNumber, SEEToken currentToken) + { + // We have to determine whether a given token is part of an issue entity. + // In order to do this, we look ahead in the token stream and construct the line we're on + // to determine whether the entity will arrive in this line or not. + IList lineTokens = + tokenList.SkipWhile(x => !ReferenceEquals(x, currentToken)).Skip(1) + .TakeWhile(x => x.TokenType != TokenType.Newline + && !x.Text.Intersect(newlineCharacters).Any()).ToList(); + string line = lineTokens.Aggregate(string.Empty, (s, t) => s + t.Text); + + foreach (IDisplayableIssue issue in issues[theLineNumber]) + { + (int startCharacter, int endCharacter)? characterRange = issue.GetCharacterRangeForLine(FilePath, theLineNumber, line); + if (!characterRange.HasValue) + { + // Switch to line-based marking instead. + IEnumerable matchTokens = lineTokens.SkipWhile(t => t.TokenType == TokenType.Whitespace); + foreach (SEEToken matchToken in matchTokens) + { + issueTokens.GetOrAdd(matchToken, () => new HashSet()).UnionWith(issues[theLineNumber]); + } + return; + } + else + { + // We have to check at which token the entity begins and at which it (inclusively) ends. + // Note that this implies that we assume an entity will always encompass only whole + // tokens, never just parts of tokens. If this doesn't hold, the whole token will be + // highlighted anyway. + + // We first create a list of character-wise parts of the tokens, then match + // using the result's index and length. + IEnumerable matchTokens = lineTokens + .SelectMany(t => Enumerable.Repeat(t, t.Text.Length)) + .Skip(characterRange.Value.startCharacter) + // Exclusive end character. + .Take(characterRange.Value.endCharacter - characterRange.Value.startCharacter - 1); + foreach (SEEToken matchToken in matchTokens) + { + issueTokens.GetOrAdd(matchToken, () => new HashSet()).Add(issue); + } + } + } + } + + // Increases the link counter to its next value. + void IncreaseLinkCounter() + { + Assert.IsTrue(linkCounter < char.MaxValue); + char[] reservedCharacters = { '<', '>', '"', '\'' }; // these characters would break our formatting + // Increase link counter until it contains an allowed character + while (reservedCharacters.Contains(++linkCounter)) + { + // intentionally left blank + } + } + + #endregion + } + + /// + /// Populates the code window with the given . + /// This will overwrite any existing text. + /// + /// An array of lines to use for the code window. + /// if true, the will be added as is, that is, + /// without being included into a noparse clause + /// If is empty or null + public void EnterFromText(string[] text, bool asIs = false) + { + if (text is not { Length: > 0 }) + { + throw new ArgumentException("Given text must not be empty or null.\n"); + } + + neededPadding = $"{text.Length}".Length; + this.text = ""; + for (int i = 0; i < text.Length; i++) + { + // Add whitespace next to line number so it's consistent. + this.text += string.Join("", Enumerable.Repeat(" ", neededPadding - $"{i + 1}".Length)); + // Line number will be typeset in yellow to distinguish it from the rest. + this.text += $"{i + 1} "; + if (asIs) + { + this.text += text[i] + "\n"; + } + else + { + this.text += $"{text[i].Replace("noparse", "")}\n"; + } + } + + lines = text.Length; + } + + /// + /// Populates the code window with the syntax-highlighted contents of the given file. + /// This will overwrite any existing text. + /// Syntax-highlighting will be done using the LSP, or Antlr if LSP is not configured for this code city. + /// + /// The platform-specific filename for the file to read. + public async UniTask EnterFromFileAsync(string filename) + { + FilePath = filename; + + // Try to read the file, otherwise display the error message. + if (!File.Exists(filename)) + { + ShowNotification.Error("File not found", $"Couldn't find file '{filename}'."); + Destroyer.Destroy(this); + return; + } + + text = "Loading code window text..."; + lines = 1; + + // TODO (#250): Maybe disable syntax highlighting for huge files, as it may impact performance badly. + using (LoadingSpinner.ShowIndeterminate($"Loading {Path.GetFileName(filename)}...")) + { + GameObject go = SceneQueries.GetCodeCity(transform).gameObject; + IEnumerable tokens; + try + { + // Usage of LSP in code windows must be configured in the LSPHandler, + // the language server must support semantic tokens, and the language of the file + // (inferred from the file extension) must be supported by the server. + if (go.TryGetComponent(out lspHandler) && lspHandler.UseInCodeWindows + && lspHandler.ServerCapabilities.SemanticTokensProvider != null + && TryGetLanguageOrLog(lspHandler, out LSPLanguage language)) + { + lspHandler.enabled = true; + lspHandler.OpenDocument(filename); + tokens = await LSPToken.FromFileAsync(filename, lspHandler, language); + } + else + { + lspHandler = null; + tokens = await AntlrToken.FromFileAsync(filename); + } + } + catch (IOException exception) + { + ShowNotification.Error("File access error", $"Couldn't access file {filename}: {exception}"); + Destroyer.Destroy(this); + return; + } + EnterFromTokens(tokens); + + if (HasStarted) + { + textMesh.SetText(text); + await UniTask.Yield(); // Wait one frame for the text meshes to be updated. + SetupBreakpoints(); + } + + if (go.TryGetComponentOrLog(out AbstractSEECity city)) + { + AssociatedGraph = city.LoadedGraph; + bool useDashboardIssues = city.ErosionSettings.ShowDashboardIssuesInCodeWindow; + bool useLspIssues = lspHandler != null && lspHandler.UseInCodeWindows; + MarkIssuesAsync(filename, useDashboardIssues, useLspIssues).Forget(); // initiate issue search in background + } + } + return; + + // Returns true iff the language for the given filename is supported by the LSP server. + bool TryGetLanguageOrLog(LSPHandler handler, out LSPLanguage language) + { + string extension = Path.GetExtension(filename).TrimStart('.'); + language = handler.Server.Languages.FirstOrDefault(x => x.FileExtensions.Contains(extension)); + if (language == null) + { + ShowNotification.Warn("Unsupported LSP language", + $"Language for extension '{extension}' not supported by the configured LSP server. " + + "Falling back to Antlr for syntax highlighting, LSP capabilities will not be available."); + return false; + } + return true; + } + } + + /// + /// Loads code issues for the file at the given path, and fills the CodeWindow + /// with the collected tokens while marking all detected issues. + /// + /// The path to the file whose issues shall be marked. + /// Whether to use issues from the Axivion Dashboard. + /// Whether to use issues from the LSP server. + private async UniTaskVoid MarkIssuesAsync(string path, bool useDashboardIssues, bool useLspIssues) + { + if (!useDashboardIssues && !useLspIssues) + { + return; + } + using (LoadingSpinner.ShowIndeterminate($"Loading issues for {Title}...")) + { + List allIssues = new(); + + if (useDashboardIssues) + { + allIssues.AddRange(await GetDashboardIssuesAsync(path)); + } + if (useLspIssues) + { + allIssues.AddRange(GetLspIssues(path)); + } + + if (allIssues.Count == 0) + { + Debug.Log($"No issues found for {path}"); + return; + } + + await UniTask.SwitchToThreadPool(); // don't interrupt main UI thread + + string queryPath = Path.GetFileName(path); + // Mapping from each line to the entities and issues contained therein. + // Important: When an entity spans over multiple lines, it's split up into one entity per line. + IDictionary> issues = + allIssues.SelectMany(issue => issue.Occurrences + .SelectMany(e => e.Range.SplitIntoLines() + .Select(range => (path, range, issue)))) + .Where(x => x.path.EndsWith(queryPath)) + .OrderBy(x => x.range.StartLine).GroupBy(x => x.range.StartLine) + .ToDictionary(x => x.Key, x => x.Select(y => y.issue).ToList()); + + EnterFromTokens(tokenList, issues); + + await UniTask.SwitchToMainThread(); + + try + { + textMesh.text = text; + textMesh.ForceMeshUpdate(); + // Will need to be marked again after the text has been updated. + MarkLine(ScrolledVisibleLine); + SetupBreakpoints(); + } + catch (IndexOutOfRangeException) + { + // FIXME (#250): Use multiple TMPs: Either one as an overlay, or split the main TMP up into multiple ones. + ShowNotification.Error("File too big", "This file is too big to be displayed correctly."); + } + } + } + + /// + /// Retrieves all issues for the given from the LSP server. + /// + /// The path of the file to get issues for. + /// A list of all issues for the given path. + private List GetLspIssues(string path) => + lspHandler.GetPublishedDiagnosticsForPath(path) + .SelectMany(x => x.Diagnostics) + .Select(x => new LSPIssue(path, x)) + .ToList(); + + /// + /// Retrieves all issues for the given from the Axivion Dashboard. + /// + /// The path of the file to get issues for. + /// A list of all issues for the given path. + private static async UniTask> GetDashboardIssuesAsync(string path) + { + string queryPath = Path.GetFileName(path); + List allIssues; + try + { + allIssues = new List(await DashboardRetriever.Instance.GetConfiguredIssuesAsync(fileFilter: $"\"*{queryPath}\"")); + } + catch (DashboardException e) + { + ShowNotification.Error("Couldn't load dashboard issues", e.Message); + return new List(); + } + + const char pathSeparator = '/'; + // When there are different paths in the issue table, this implies that there are some files + // which aren't actually the one we're looking for (because we've only matched by filename so far). + // In this case, we'll gradually refine our results until this isn't the case anymore. + for (int skippedParts = path.Count(x => x == pathSeparator) - 2; !AllMatchingPaths(allIssues); skippedParts--) + { + Assert.IsTrue(path.Contains(pathSeparator)); + // Skip the first skippedParts parts, so that we query progressively larger parts. + queryPath = string.Join(pathSeparator.ToString(), path.Split(pathSeparator).Skip(skippedParts)); + allIssues.RemoveAll(x => !x.Occurrences.Any(e => e.Path.EndsWith(queryPath))); + } + + return allIssues; + + // Returns true iff all issues are on the same path. + static bool AllMatchingPaths(ICollection issues) + { + if (!issues.Any()) + { + return true; + } + // Every path in the first issue could be the "right" path, so we try them all. + // If every issue has at least one path which matches that one, we can return true. + return issues.First().Occurrences.Select(e => e.Path) + .Any(path => issues.All(x => x.Occurrences.Any(e => e.Path == path))); + } + } + } +} diff --git a/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs.meta b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs.meta new file mode 100644 index 0000000000..24a41d9ed3 --- /dev/null +++ b/Assets/SEE/UI/Window/CodeWindow/CodeWindowInput.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24e6c0c71bc5d636e9ca95845011d464 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs index a6135556c1..888fb55c91 100644 --- a/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs +++ b/Assets/SEE/UI/Window/CodeWindow/DesktopCodeWindow.cs @@ -13,9 +13,11 @@ using UnityEngine.UI; using System.Collections.Generic; using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages; -using Michsky.UI.ModernUIPack; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; using SEE.Controls; using SEE.UI.DebugAdapterProtocol; +using SEE.Utils.Markdown; +using UnityEngine.Assertions; namespace SEE.UI.Window.CodeWindow { @@ -52,6 +54,12 @@ protected override void StartDesktop() scrollable = PrefabInstantiator.InstantiatePrefab(codeWindowPrefab, Window.transform.Find("Content"), false); scrollable.name = "Scrollable"; + // Initialize context menu, if necessary. + if (lspHandler != null) + { + contextMenu = ContextMenuHandler.FromCodeWindow(this); + } + // Set text and preferred font size GameObject code = scrollable.transform.Find("Code").gameObject; if (code.TryGetComponentOrLog(out textMesh)) @@ -64,8 +72,9 @@ protected override void StartDesktop() DebugBreakpointManager.OnBreakpointAdded += OnBreakpointAdded; DebugBreakpointManager.OnBreakpointRemoved += OnBreakpointRemoved; - Transform temp = SceneQueries.GetCodeCity(transform); - if (temp && temp.gameObject.TryGetComponentOrLog(out AbstractSEECity city)) { + Transform cityTransform = SceneQueries.GetCodeCity(transform); + if (cityTransform && cityTransform.gameObject.TryGetComponentOrLog(out AbstractSEECity city)) + { // Get button for IDE interaction and register events. Window.transform.Find("Dragger/IDEButton").gameObject.GetComponent public static int MainThreadId = 0; + /// + /// Converts the given to an asynchronous UniTask enumerable. + /// + /// The task of enumerables to convert. + /// The type of the elements in the enumerable. + /// An asynchronous UniTask enumerable that emits the elements of the enumerable. + public static IUniTaskAsyncEnumerable AsUniTaskAsyncEnumerable(this UniTask> task) + { + return task.ToUniTaskAsyncEnumerable().SelectMany(x => x.ToUniTaskAsyncEnumerable()); + } + /// /// Runs the given with a and returns the result. /// Note that a timeout of will cause no timeout to be applied. @@ -53,7 +64,7 @@ public static async UniTask RunWithTimeoutAsync(Func(this ISet set, T element) /// /// Gets the value for the given from the given . /// If the key is not present in the dictionary, the given - /// will be added to the dictionary and returned. + /// will be evaluated and its result added to the dictionary and returned. /// /// The dictionary from which the value shall be retrieved. /// The key for which the value shall be retrieved. - /// The default value which shall be added to the dictionary if the key is not present. + /// A lambda returning the default value which shall be added to the dictionary if the key is not present. /// The type of the keys in the dictionary. /// The type of the values in the dictionary. /// The value for the given from the given . - public static V GetOrAdd(this IDictionary dict, K key, V defaultValue) + public static V GetOrAdd(this IDictionary dict, K key, Func defaultValue) { if (dict.TryGetValue(key, out V value)) { @@ -45,7 +45,7 @@ public static V GetOrAdd(this IDictionary dict, K key, V defaultValue } else { - return dict[key] = defaultValue; + return dict[key] = defaultValue(); } } diff --git a/Assets/SEE/Utils/DefaultDictionary.cs b/Assets/SEE/Utils/DefaultDictionary.cs index 129b29980a..4368445a7c 100644 --- a/Assets/SEE/Utils/DefaultDictionary.cs +++ b/Assets/SEE/Utils/DefaultDictionary.cs @@ -13,7 +13,7 @@ namespace SEE.Utils { public new V this[K key] { - get => this.GetOrAdd(key, new V()); + get => this.GetOrAdd(key, () => new V()); set => base[key] = value; } } diff --git a/Assets/SEE/Utils/Icons.cs b/Assets/SEE/Utils/Icons.cs index 7c87efb40d..1c3924ef30 100644 --- a/Assets/SEE/Utils/Icons.cs +++ b/Assets/SEE/Utils/Icons.cs @@ -10,37 +10,55 @@ namespace SEE.Utils /// public static class Icons { - public const char Node = '\uF1B2'; - public const char Edge = '\uF542'; - public const char OutgoingEdge = '\uF2F5'; - public const char IncomingEdge = '\uF2F6'; - public const char LiftedIncomingEdge = '\uF090'; - public const char LiftedOutgoingEdge = '\uF08B'; - public const char EmptyCheckbox = '\uF0C8'; + public const char ArrowRotateLeft = '\uF0E2'; + public const char Bars = '\uF0C9'; + public const char Broadcast = '\uF519'; + public const char Chalkboard = '\uF51B'; public const char CheckedCheckbox = '\uF14A'; - public const char MinusCheckbox = '\uF146'; + public const char CheckedRadio = '\uF192'; public const char Checkmark = '\uF00C'; - public const char Trash = '\uF1F8'; - public const char Info = '\uF05A'; - public const char LightBulb = '\uF0EB'; + public const char CircleCheckmark = '\uF058'; + public const char CircleExclamationMark = '\uF06A'; + public const char CircleMinus = '\uF056'; + public const char CircleQuestionMark = '\uF059'; + public const char Crosshairs = '\uF05B'; public const char Code = '\uF121'; - public const char TreeView = '\uF802'; public const char Compare = '\uE13A'; + public const char Edge = '\uF542'; + public const char EmptyCheckbox = '\uF0C8'; + public const char EmptyRadio = '\uF111'; + public const char Export = '\uF56E'; + public const char Eye = '\uF06E'; + public const char EyeSlash = '\uF070'; + public const char Gear = '\uF013'; + public const char Hashtag = '#'; public const char Hide = '\uF070'; - public const char Show = '\uF06E'; + public const char Import = '\uF56F'; + public const char IncomingEdge = '\uF2F6'; + public const char Info = '\uF05A'; + public const char LiftedIncomingEdge = '\uF090'; + public const char LiftedOutgoingEdge = '\uF08B'; + public const char LightBulb = '\uF0EB'; + public const char Link = '\uF0C1'; + public const char MagnifyingGlass = '\uF002'; + public const char MinusCheckbox = '\uF146'; + public const char Move = '\uF0B2'; + public const char Node = '\uF1B2'; + public const char OutgoingEdge = '\uF2F5'; + public const char PenToSquare = '\uF044'; + public const char Pencil = '\uF303'; public const char QuestionMark = '?'; - public const char ArrowRotateLeft = '\uF0E2'; - public const char SortAlphabeticalUp = '\uF15E'; + public const char Rotate = '\uF2F1'; + public const char Scale = '\uF424'; + public const char Show = '\uF06E'; + public const char Sitemap = '\uF0E8'; public const char SortAlphabeticalDown = '\uF15D'; - public const char SortNumericUp = '\uF163'; + public const char SortAlphabeticalUp = '\uF15E'; public const char SortNumericDown = '\uF162'; - public const char Hashtag = '#'; + public const char SortNumericUp = '\uF163'; public const char Text = '\uF031'; - public const char EmptyRadio = '\uF111'; - public const char CheckedRadio = '\uF192'; - public const char CircleMinus = '\uF056'; - public const char CircleCheckmark = '\uF058'; - public const char CircleQuestionMark = '\uF059'; - public const char CircleExclamationMark = '\uF06A'; + public const char Trash = '\uF1F8'; + public const char TreeView = '\uF802'; + public const char VR = '\uF729'; } } diff --git a/Assets/SEE/Utils/Markdown.meta b/Assets/SEE/Utils/Markdown.meta new file mode 100644 index 0000000000..102e9410e0 --- /dev/null +++ b/Assets/SEE/Utils/Markdown.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 11cb6722d4dd45388ae5025556cb13d3 +timeCreated: 1721406915 \ No newline at end of file diff --git a/Assets/SEE/Utils/Markdown/MarkdownConverter.cs b/Assets/SEE/Utils/Markdown/MarkdownConverter.cs new file mode 100644 index 0000000000..449611cb10 --- /dev/null +++ b/Assets/SEE/Utils/Markdown/MarkdownConverter.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MoreLinq.Extensions; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using UnityEngine; + +namespace SEE.Utils.Markdown +{ + /// + /// Utility class for converting markdown text to TextMeshPro-compatible rich text. + /// + public static class MarkdownConverter + { + /// + /// Converts the given to TextMeshPro-compatible rich text. + /// + /// The content to convert. + /// The converted rich text. + public static string ToRichText(this MarkedStringsOrMarkupContent content) + { + string markdown; + if (content.HasMarkupContent) + { + MarkupContent markup = content.MarkupContent!; + switch (markup.Kind) + { + case MarkupKind.PlainText: return $"{markup.Value}"; + case MarkupKind.Markdown: + markdown = markup.Value; + break; + default: + Debug.LogError($"Unsupported markup kind: {markup.Kind}"); + return string.Empty; + } + } + else + { + // This is technically deprecated, but we still need to support it, + // since some language servers still use it. + Container strings = content.MarkedStrings!; + markdown = string.Join("\n", strings.Select(x => + { + if (x.Language != null) + { + return $"```{x.Language}\n{x.Value}\n```"; + } + else + { + return x.Value; + } + })); + } + + string richText = MarkupTextToRichText(markdown); + // We concatenate empty successive lines, which may sometimes appear in the converted rich text. + // To check if a line is empty, we need to get rid of its tags first. + return string.Join('\n', richText.Split('\n') + // We start a new segment whenever the line does not only consist of + // white space. + .Segment(x => !string.IsNullOrWhiteSpace(x.WithoutRichTextTags())) + // Then, we join the segments with a single line break. + // This way, we make sure not to accidentally remove rich text tags. + .Select(HandleSegment)); + + string HandleSegment(IEnumerable segment) + { + IList lines = segment.ToList(); + if (lines.Count == 1) + { + return lines[0]; + } + else + { + // First line should be separated by a line break so that at least one line break is present. + return lines[0] + '\n' + string.Join(string.Empty, lines.Skip(1)); + } + } + } + + /// + /// Converts the given markdown-formatted to TextMeshPro-compatible rich text. + /// + /// The markdown-formatted text to convert. + /// The converted rich text. + public static string MarkupTextToRichText(string markdownText) + { + StringWriter writer = new(); + Markdig.Markdown.Convert(markdownText, new RichTagsMarkdownRenderer(writer)); + return writer.ToString(); + } + } +} diff --git a/Assets/SEE/Utils/Markdown/MarkdownConverter.cs.meta b/Assets/SEE/Utils/Markdown/MarkdownConverter.cs.meta new file mode 100644 index 0000000000..8ebfdbbbba --- /dev/null +++ b/Assets/SEE/Utils/Markdown/MarkdownConverter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e8d95f54711e46b8b8412cf985ddf38d +timeCreated: 1721398270 \ No newline at end of file diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs b/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs new file mode 100644 index 0000000000..7dc56fa9ea --- /dev/null +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs @@ -0,0 +1,95 @@ +using System; +using Markdig.Syntax; + +namespace SEE.Utils.Markdown +{ + /// + /// Partial class that contains renderers for block Markdown elements. + /// + public partial class RichTagsMarkdownRenderer + { + /// + /// Renders a code block. + /// + private class CodeBlockRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, CodeBlock obj) + { + renderer.EnsureLine(); + + renderer.Write(""); + renderer.WriteStringLines(obj.Lines); + renderer.Write(""); + + renderer.EnsureLine(); + } + } + + /// + /// Renders a heading. + /// + private class HeadingRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, HeadingBlock obj) + { + renderer.EnsureLine(); + + renderer.Write($""); + renderer.WriteLeafInline(obj); + renderer.Write(""); + + renderer.EnsureLine(); + } + } + + /// + /// Renders a quote block. + /// + private class QuoteRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, QuoteBlock obj) + { + renderer.EnsureLine(); + + renderer.Write(""); + renderer.WriteChildren(obj); + renderer.Write(""); + + renderer.EnsureLine(); + } + } + + /// + /// Renders a list block. + /// + private class ListRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, ListBlock obj) + { + const char bullet = '•'; + renderer.EnsureLine(); + foreach (Block block in obj) + { + renderer.EnsureLine(); + renderer.Write(bullet); + renderer.Write(' '); + renderer.WriteChildren((ListItemBlock)block); + renderer.EnsureLine(); + } + renderer.EnsureLine(); + } + } + + /// + /// Renders a paragraph block. + /// + private class ParagraphRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, ParagraphBlock obj) + { + renderer.WriteLeafInline(obj); + renderer.EnsureLine(); + } + } + } +} diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs.meta b/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs.meta new file mode 100644 index 0000000000..af0cde443c --- /dev/null +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownBlockRenderers.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dc13532ab7174e73ac01a4c90a1b5fa9 +timeCreated: 1721406969 \ No newline at end of file diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs b/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs new file mode 100644 index 0000000000..14c4c6bbb8 --- /dev/null +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs @@ -0,0 +1,94 @@ +using Markdig.Syntax.Inlines; + +namespace SEE.Utils.Markdown +{ + /// + /// Partial class that contains renderers for inline Markdown elements. + /// + public partial class RichTagsMarkdownRenderer + { + /// + /// Renders an inline code span. + /// + private class CodeInlineRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, CodeInline obj) + { + renderer.Write(""); + renderer.WriteEscaped(obj.ContentSpan); + renderer.Write(""); + } + } + + /// + /// Renders a delimiter inline. + /// + private class DelimiterInlineRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, DelimiterInline obj) + { + renderer.WriteEscaped(obj.ToLiteral()); + renderer.WriteChildren(obj); + } + } + + /// + /// Renders an emphasized span of text. + /// + private class EmphasisInlineRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, EmphasisInline obj) + { + if (obj.DelimiterCount == 1) + { + renderer.Write(""); + renderer.WriteChildren(obj); + renderer.Write(""); + } + else + { + renderer.Write(""); + renderer.WriteChildren(obj); + renderer.Write(""); + } + } + } + + /// + /// Renders a line break. + /// + private class LineBreakInlineRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, LineBreakInline obj) + { + renderer.EnsureLine(); + } + } + + /// + /// Renders a link. + /// + private class LinkInlineRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, LinkInline obj) + { + // Links are hard to emulate with TextMeshPro, so we'll just use the URL as the text + // in parentheses. + string url = obj.GetDynamicUrl?.Invoke() ?? obj.Url; + renderer.WriteChildren(obj); + renderer.Write($" ({url})"); + } + } + + /// + /// Renders a literal inline. + /// + private class LiteralInlineRenderer : RichTagsObjectRenderer + { + protected override void Write(RichTagsMarkdownRenderer renderer, LiteralInline obj) + { + renderer.WriteEscaped(obj.Content.AsSpan()); + } + } + } +} diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs.meta b/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs.meta new file mode 100644 index 0000000000..7269a24ed5 --- /dev/null +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownInlineRenderers.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9626b79902b1428d8fb26a6f8ed59f7a +timeCreated: 1721410960 \ No newline at end of file diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs b/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs new file mode 100644 index 0000000000..c358ce9a4e --- /dev/null +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using JetBrains.Annotations; +using Markdig.Helpers; +using Markdig.Renderers; +using Markdig.Syntax; + +namespace SEE.Utils.Markdown +{ + /// + /// A custom Markdig renderer that renders Markdown input to TextMeshPro-compatible rich text. + /// + public partial class RichTagsMarkdownRenderer : TextRendererBase + { + public RichTagsMarkdownRenderer([NotNull] TextWriter writer) : base(writer) + { + // Default block renderers + ObjectRenderers.Add(new CodeBlockRenderer()); + ObjectRenderers.Add(new ListRenderer()); + ObjectRenderers.Add(new HeadingRenderer()); + ObjectRenderers.Add(new ParagraphRenderer()); + ObjectRenderers.Add(new QuoteRenderer()); + + // Default inline renderers + ObjectRenderers.Add(new CodeInlineRenderer()); + ObjectRenderers.Add(new DelimiterInlineRenderer()); + ObjectRenderers.Add(new EmphasisInlineRenderer()); + ObjectRenderers.Add(new LineBreakInlineRenderer()); + ObjectRenderers.Add(new LinkInlineRenderer()); + ObjectRenderers.Add(new LiteralInlineRenderer()); + } + + /// + /// Writes the given of string lines. + /// + /// The group of string lines to write. + private void WriteStringLines(StringLineGroup group) + { + StringLine[] lines = group.Lines; + // IDEs wrongly suggest that `lines` cannot be null, but it can be. + // ReSharper disable ConditionIsAlwaysTrueOrFalse + // ReSharper disable HeuristicUnreachableCode + if (lines is null) + { + return; + } + + for (int i = 0; i < lines.Length; i++) + { + if (lines[i].Slice.Text != null && lines[i].Slice.IsEmptyOrWhitespace()) + { + // If this line is empty, we shouldn't write anything. + continue; + } + if (i > 0) + { + WriteLine(); + } + WriteEscaped(lines[i].Slice.AsSpan()); + } + } + + /// + /// Writes the given of characters, surrounded by a <noparse> tag. + /// + /// The span of characters to write. + private void WriteEscaped(ReadOnlySpan span) + { + Write(""); + Write(span); + Write(""); + } + + /// + /// An object renderer that renders -typed instances of Markdown objects + /// as TextMeshPro-compatible rich text. + /// + /// The type of Markdown object to render. + private abstract class RichTagsObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject + { + } + } +} diff --git a/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs.meta b/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs.meta new file mode 100644 index 0000000000..fef52599e8 --- /dev/null +++ b/Assets/SEE/Utils/Markdown/RichTagsMarkdownRenderer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a737f666276740f2b24df601f460a7aa +timeCreated: 1721406928 \ No newline at end of file diff --git a/Assets/SEE/Utils/ReferenceEqualityComparer.cs b/Assets/SEE/Utils/ReferenceEqualityComparer.cs new file mode 100644 index 0000000000..9eb178f33a --- /dev/null +++ b/Assets/SEE/Utils/ReferenceEqualityComparer.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace SEE.Utils +{ + // NOTE: The below class was copied from the .NET 5.x source code. + // Unity uses .NET 4.x, so this class would otherwise not be available. + + // Original license for this code: + // The MIT License (MIT) + // Copyright (c) .NET Foundation and Contributors + // + // All rights reserved. + // + // Permission is hereby granted, free of charge, to any person obtaining a copy + // of this software and associated documentation files (the "Software"), to deal + // in the Software without restriction, including without limitation the rights + // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + // copies of the Software, and to permit persons to whom the Software is + // furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in all + // copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + // SOFTWARE. + + /// + /// An that uses reference equality () + /// instead of value equality () when comparing two object instances. + /// + /// + /// The type cannot be instantiated. Instead, use the property + /// to access the singleton instance of this type. + /// + public sealed class ReferenceEqualityComparer : IEqualityComparer + { + private ReferenceEqualityComparer() { } + + /// + /// Gets the singleton instance. + /// + public static ReferenceEqualityComparer Instance { get; } = new(); + + /// + /// Determines whether two object references refer to the same object instance. + /// + /// The first object to compare. + /// The second object to compare. + /// + /// if both and refer to the same object instance + /// or if both are ; otherwise, . + /// + /// + /// This API is a wrapper around . + /// It is not necessarily equivalent to calling . + /// + public new bool Equals(object x, object y) => ReferenceEquals(x, y); + + /// + /// Returns a hash code for the specified object. The returned hash code is based on the object + /// identity, not on the contents of the object. + /// + /// The object for which to retrieve the hash code. + /// A hash code for the identity of . + /// + /// This API is a wrapper around . + /// It is not necessarily equivalent to calling . + /// + public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); + } +} diff --git a/Assets/SEE/Utils/ReferenceEqualityComparer.cs.meta b/Assets/SEE/Utils/ReferenceEqualityComparer.cs.meta new file mode 100644 index 0000000000..8240b05faf --- /dev/null +++ b/Assets/SEE/Utils/ReferenceEqualityComparer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ac6b1291562446a8991abbd11d8f0af6 +timeCreated: 1720735805 \ No newline at end of file diff --git a/Assets/SEE/Utils/StringExtensions.cs b/Assets/SEE/Utils/StringExtensions.cs index cf9931bf96..518c59383e 100644 --- a/Assets/SEE/Utils/StringExtensions.cs +++ b/Assets/SEE/Utils/StringExtensions.cs @@ -1,4 +1,6 @@ using System.Text; +using System.Text.RegularExpressions; +using UnityEngine; namespace SEE.Utils { @@ -17,19 +19,19 @@ public static class StringExtensions /// wrapping word-wise. public static string WrapLines(this string input, int wrapAt) { - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); foreach (string inputLine in input.Split('\n')) { string line = inputLine; while (line.Length >= wrapAt) { - int lastSpace = line.Substring(0, wrapAt).LastIndexOf(' '); + int lastSpace = line[..wrapAt].LastIndexOf(' '); if (lastSpace == -1) { lastSpace = wrapAt; } - builder.Append(line.Substring(0, lastSpace) + '\n'); - line = line.Substring(lastSpace).TrimStart(' '); + builder.Append(line[..lastSpace] + '\n'); + line = line[lastSpace..].TrimStart(' '); } builder.Append(line + '\n'); @@ -37,5 +39,62 @@ public static string WrapLines(this string input, int wrapAt) return builder.Remove(builder.Length - 1, 1).ToString().TrimEnd('\n'); // remove excess newlines } + + /// + /// All supported rich text tags. + /// + private static readonly string[] richTextTags = + { + "align", "allcaps", "alpha", "b", "color", "cspace", "font", "font-weight", "gradient", "i", "indent", + "line-height", "line-indent", "link", "lowercase", "margin", "mark", "mspace", "nobr", + "page", "pos", "rotate", "s", "size", "smallcaps", "space", "sprite", "strikethrough", "sub", "sup", + "u", "uppercase", "voffset" + }; + + /// + /// A regex that matches all rich text tags. + /// + private static readonly Regex richTextTagsRegex = new($"]*?>", + RegexOptions.Compiled); + + /// + /// Removes all rich text tags from this string. + /// Note that content inside tags will be ignored. + /// + /// The string to clean. + /// The string without any rich text tags. + public static string WithoutRichTextTags(this string input) + { + StringBuilder builder = new(); + string[] segments = Regex.Split(input, "()"); + int noparseCount = 0; + foreach (string segment in segments) + { + if (segment == "") + { + noparseCount++; + } + else if (segment == "") + { + noparseCount--; + } + else if (noparseCount == 0) + { + // We need to delete all tags in here. + builder.Append(richTextTagsRegex.Replace(segment, "")); + } + else + { + builder.Append(segment); + } + } + + if (noparseCount > 0) + { + Debug.LogWarning("Unbalanced tags in rich text. Original text:\n" + input); + } + + return builder.ToString(); + } } -} \ No newline at end of file +} diff --git a/Assets/SEEPlayModeTests/TestMenu.cs b/Assets/SEEPlayModeTests/TestMenu.cs index 4a198d529a..514fca50c2 100644 --- a/Assets/SEEPlayModeTests/TestMenu.cs +++ b/Assets/SEEPlayModeTests/TestMenu.cs @@ -73,18 +73,23 @@ internal abstract class TestMenu : TestUI /// protected const float TimeUntilMenuIsSetup = 1f; + /// + /// An example icon. + /// + public const char ExampleIcon = Icons.Move; + /// /// Path to a sprite we can use for testing. /// - private const string PathOfIcon = "Materials/Charts/MoveIcon"; + private const string PathOfIconSprite = "Materials/Charts/MoveIcon"; /// - /// The icon loaded from . + /// The icon loaded from . /// - /// icon loaded from - protected static Sprite GetIcon() + /// icon loaded from + protected static Sprite GetIconSprite() { - return Resources.Load(PathOfIcon); + return Resources.Load(PathOfIconSprite); } /// @@ -122,4 +127,4 @@ protected static void PressCloseButton(string menuTitle) PressButton($"/UI Canvas/{menuTitle}/Main Content/Buttons/Content/Close"); } } -} \ No newline at end of file +} diff --git a/Assets/SEEPlayModeTests/TestNestedMenu.cs b/Assets/SEEPlayModeTests/TestNestedMenu.cs index ee0ae46b8a..018cb8fda4 100644 --- a/Assets/SEEPlayModeTests/TestNestedMenu.cs +++ b/Assets/SEEPlayModeTests/TestNestedMenu.cs @@ -1,8 +1,7 @@ -using NUnit.Framework; -using System.Collections; +using System.Collections; using System.Collections.Generic; +using NUnit.Framework; using UnityEngine; -using UnityEngine.Events; using UnityEngine.TestTools; namespace SEE.UI.Menu @@ -53,7 +52,7 @@ internal class TestNestedMenu : TestMenu /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuOption1() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -68,7 +67,7 @@ public IEnumerator TestMenuOption1() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuNestedOptionOne() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -85,7 +84,7 @@ public IEnumerator TestMenuNestedOptionOne() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuNestedOptionTwo() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -102,7 +101,7 @@ public IEnumerator TestMenuNestedOptionTwo() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuNoOption() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -127,39 +126,32 @@ protected override void CreateMenu(out GameObject menuGO, out SimpleListMenu menuEntries = new List { - new MenuEntry(selectAction: new UnityAction(() => { selection = OptionOneValue; }), - unselectAction: null, - title: OptionOne, - description: "Select option 1", - entryColor: Color.red, - enabled: true, - icon: GetIcon()), - new NestedMenuEntry(innerEntries: new List() - { - new MenuEntry(selectAction: new UnityAction(() => { selection = NestedOptionOneValue; }), - unselectAction: null, - title: NestedOptionOne, - description: "Select option 2a", - entryColor: Color.green, - enabled: true, - icon: GetIcon()), - new MenuEntry(selectAction: new UnityAction(() => { selection = NestedOptionTwoValue; }), - unselectAction: null, - title: NestedOptionTwo, - description: "Select option 2b", - entryColor: Color.green, - enabled: true, - icon: GetIcon()) + new(SelectAction: () => { selection = OptionOneValue; }, + Title: OptionOne, + Description: "Select option 1", + EntryColor: Color.red, + Icon: ExampleIcon), + new NestedMenuEntry(innerEntries: new List + { + new(SelectAction: () => selection = NestedOptionOneValue, + Title: NestedOptionOne, + Description: "Select option 2a", + EntryColor: Color.green, + Icon: ExampleIcon), + new(SelectAction: () => selection = NestedOptionTwoValue, + Title: NestedOptionTwo, + Description: "Select option 2b", + EntryColor: Color.green, + Icon: ExampleIcon) }, title: SubMenuTitle, description: "open subselection 2", entryColor: Color.red, - enabled: true, - icon: GetIcon()) + icon: ExampleIcon) }; menu.AddEntries(menuEntries); diff --git a/Assets/SEEPlayModeTests/TestSimpleMenu.cs b/Assets/SEEPlayModeTests/TestSimpleMenu.cs index bfcdd60d6f..0a6d114450 100644 --- a/Assets/SEEPlayModeTests/TestSimpleMenu.cs +++ b/Assets/SEEPlayModeTests/TestSimpleMenu.cs @@ -1,8 +1,7 @@ -using NUnit.Framework; -using System.Collections; +using System.Collections; using System.Collections.Generic; +using NUnit.Framework; using UnityEngine; -using UnityEngine.Events; using UnityEngine.TestTools; namespace SEE.UI.Menu @@ -16,10 +15,12 @@ internal class TestSimpleMenu : TestMenu /// Title of option 1 in the menu. /// private const string OptionOne = "Option 1"; + /// /// Title of option 2 in the menu. /// private const string OptionTwo = "Option 2"; + /// /// Title of the menu. /// @@ -29,7 +30,7 @@ internal class TestSimpleMenu : TestMenu /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuOption1() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -44,7 +45,7 @@ public IEnumerator TestMenuOption1() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuOption2() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -59,7 +60,7 @@ public IEnumerator TestMenuOption2() /// /// [UnityTest] - [LoadScene()] + [LoadScene] public IEnumerator TestMenuNoOption() { yield return new WaitForSeconds(TimeUntilMenuIsSetup); @@ -83,24 +84,20 @@ protected override void CreateMenu(out GameObject menuGO, out SimpleListMenu menuEntries = new List { - new MenuEntry(selectAction: new UnityAction(() => { selection = 1; }), - unselectAction: null, - title: OptionOne, - description: "Select option 1", - entryColor: Color.red, - enabled: true, - icon: GetIcon()), - new MenuEntry(selectAction: new UnityAction(() => { selection = 2; }), - unselectAction: null, - title: OptionTwo, - description: "Select option 2", - entryColor: Color.green, - enabled: true, - icon: GetIcon()), + new(SelectAction: () => selection = 1, + Title: OptionOne, + Description: "Select option 1", + EntryColor: Color.red, + Icon: ExampleIcon), + new(SelectAction: () => selection = 2, + Title: OptionTwo, + Description: "Select option 2", + EntryColor: Color.green, + Icon: ExampleIcon), }; menu.AddEntries(menuEntries); diff --git a/Assets/SEETests/TestActionHistory.cs b/Assets/SEETests/TestActionHistory.cs index 6cb732a94e..635165548b 100644 --- a/Assets/SEETests/TestActionHistory.cs +++ b/Assets/SEETests/TestActionHistory.cs @@ -8,7 +8,7 @@ namespace SEE.Utils.History /// /// Test cases for . /// - class TestActionHistory + internal class TestActionHistory { /// /// Shortcut to a constructor of that requires @@ -23,7 +23,7 @@ private abstract class TestActionStateType : ActionStateType /// value for protected TestActionStateType(string name, CreateReversibleAction createReversible) : base(name: name, description: "", color: UnityEngine.Color.white, - iconPath: "", createReversible: createReversible, register: false) + icon: ' ', createReversible: createReversible, register: false) { } } diff --git a/Assets/SEETests/TestActionStateType.cs b/Assets/SEETests/TestActionStateType.cs index 9d9d2a9295..638d535bcb 100644 --- a/Assets/SEETests/TestActionStateType.cs +++ b/Assets/SEETests/TestActionStateType.cs @@ -56,8 +56,8 @@ public void ActionStateTypesAllRootTypesJustContainsAllRoots() [Test] public void TestNoAttributeNull() { - Assert.IsEmpty(allRootTypes.AllElements().Where(x => x.Description == null || x.Name == null || x.IconPath == null), - "No attribute of an AbstractActionStateType may be null!"); + Assert.IsEmpty(allRootTypes.AllElements().Where(x => x.Description == null || x.Name == null || x.Icon == default), + "No attribute of an AbstractActionStateType may be null or default!"); } [Test] @@ -81,4 +81,4 @@ public void TestEquality(AbstractActionStateType type) "An ActionStateType must only be equal to itself!"); } } -} \ No newline at end of file +} diff --git a/Assets/SEETests/TestConfigIO.cs b/Assets/SEETests/TestConfigIO.cs index fcdd9dfd40..00dd966a59 100644 --- a/Assets/SEETests/TestConfigIO.cs +++ b/Assets/SEETests/TestConfigIO.cs @@ -966,7 +966,7 @@ private static void WipeOutErosionSettings(AbstractSEECity city) { city.ErosionSettings.ShowInnerErosions = !city.ErosionSettings.ShowInnerErosions; city.ErosionSettings.ShowLeafErosions = !city.ErosionSettings.ShowLeafErosions; - city.ErosionSettings.ShowIssuesInCodeWindow = !city.ErosionSettings.ShowIssuesInCodeWindow; + city.ErosionSettings.ShowDashboardIssuesInCodeWindow = !city.ErosionSettings.ShowDashboardIssuesInCodeWindow; city.ErosionSettings.ErosionScalingFactor++; city.ErosionSettings.StyleIssue = "X"; @@ -994,7 +994,7 @@ private static void AreEqualErosionSettings(ErosionAttributes expected, ErosionA { Assert.AreEqual(expected.ShowInnerErosions, actual.ShowInnerErosions); Assert.AreEqual(expected.ShowLeafErosions, actual.ShowLeafErosions); - Assert.AreEqual(expected.ShowIssuesInCodeWindow, actual.ShowIssuesInCodeWindow); + Assert.AreEqual(expected.ShowDashboardIssuesInCodeWindow, actual.ShowDashboardIssuesInCodeWindow); Assert.AreEqual(expected.ErosionScalingFactor, actual.ErosionScalingFactor); Assert.AreEqual(expected.StyleIssue, actual.StyleIssue); diff --git a/Assets/SEETests/TestTokenMetrics.cs b/Assets/SEETests/TestTokenMetrics.cs index 10abc030ca..926b6277e0 100644 --- a/Assets/SEETests/TestTokenMetrics.cs +++ b/Assets/SEETests/TestTokenMetrics.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using System.Collections.Generic; +using SEE.Scanner.Antlr; namespace SEE.Scanner { @@ -43,7 +44,7 @@ public int Add(int a, int b) [TestCase("public class DoesNotCompile\n{\n break; continue; case 2: while do if else foreach for switch try catch }", 7)] public void TestCalculateMcCabeComplexity(string code, int expected) { - IEnumerable tokens = SEEToken.FromString(code, TokenLanguage.CSharp); + IEnumerable tokens = AntlrToken.FromString(code, AntlrLanguage.CSharp); int complexity = TokenMetrics.CalculateMcCabeComplexity(tokens); Assert.AreEqual(expected, complexity); } @@ -59,7 +60,7 @@ public void TestCalculateHalsteadMetrics() // Test case for empty code, in case DistinctOperators, DistinctOperands and/or ProgramVocabulary values are zero. string emptyCode = ""; - IEnumerable tokensEmptyCode = SEEToken.FromString(emptyCode, TokenLanguage.Plain); + IList tokensEmptyCode = AntlrToken.FromString(emptyCode, AntlrLanguage.Plain); TokenMetrics.HalsteadMetrics expectedEmptyCode = new(DistinctOperators: 0, DistinctOperands: 0, TotalOperators: 0, @@ -85,7 +86,7 @@ public static void main(String[] args) { } }"; - IEnumerable tokens = SEEToken.FromString(code, TokenLanguage.Java); + IList tokens = AntlrToken.FromString(code, AntlrLanguage.Java); TokenMetrics.HalsteadMetrics expected = new(DistinctOperators: 11, DistinctOperands: 16, TotalOperators: 17, @@ -116,7 +117,7 @@ public static void main(String[] args) { // Test case for code with no operators to test Plain Text. string codeWithNoOperators = "This arbitary file has no code.\nJust plain words."; // "." is its own operand. - IEnumerable tokensNoOperators = SEEToken.FromString(codeWithNoOperators, TokenLanguage.Plain); + IList tokensNoOperators = AntlrToken.FromString(codeWithNoOperators, AntlrLanguage.Plain); TokenMetrics.HalsteadMetrics expectedNoOperators = new(DistinctOperators: 0, DistinctOperands: 10, TotalOperators: 0, @@ -164,7 +165,7 @@ void setX(int y) { [TestCase(" ", 0)] public void TestCalculateLinesOfCode(string code, int expected) { - IEnumerable tokens = SEEToken.FromString(code, TokenLanguage.CPP); + IEnumerable tokens = AntlrToken.FromString(code, AntlrLanguage.CPP); int linesOfCode = TokenMetrics.CalculateLinesOfCode(tokens); Assert.AreEqual(expected, linesOfCode); } diff --git a/Assets/SEETests/UI/TestMenuEntry.cs b/Assets/SEETests/UI/TestMenuEntry.cs index 8f1f1440ed..081489891f 100644 --- a/Assets/SEETests/UI/TestMenuEntry.cs +++ b/Assets/SEETests/UI/TestMenuEntry.cs @@ -12,55 +12,47 @@ namespace SEE.UI.Menu internal class TestMenuEntry { /// - /// Path to a sprite we can use for testing. + /// An icon used for testing. /// - private const string TEST_SPRITE = "Materials/Charts/MoveIcon"; + private const char testIcon = '!'; protected static IEnumerable ValidConstructorSupplier() { - Sprite testSprite = Resources.Load(TEST_SPRITE); - yield return new TestCaseData(new UnityAction(() => { }), "Test", "Test description", Color.red, - true, testSprite); + yield return new TestCaseData(new Action(() => { }), "Test", "Test description", Color.red, + true, testIcon); yield return new TestCaseData(null, "Test", "Test description", Color.green, - true, testSprite); - yield return new TestCaseData(new UnityAction(() => { }), "Test", null, Color.blue, - true, testSprite); - yield return new TestCaseData(new UnityAction(() => { }), "Test", "Test description", null, - true, testSprite); - yield return new TestCaseData(new UnityAction(() => { }), "Test", "Test description", Color.white, - false, testSprite); - yield return new TestCaseData(new UnityAction(() => { }), "Test", "Test description", Color.black, - true, null); - yield return new TestCaseData(null, "Test", null, null, true, null); + true, testIcon); + yield return new TestCaseData(new Action(() => { }), "Test", null, Color.blue, + true, testIcon); + yield return new TestCaseData(new Action(() => { }), "Test", "Test description", null, + true, testIcon); + yield return new TestCaseData(new Action(() => { }), "Test", "Test description", Color.white, + false, testIcon); + yield return new TestCaseData(new Action(() => { }), "Test", "Test description", Color.black, + true, ' '); + yield return new TestCaseData(null, "Test", null, null, true, ' '); } /// /// Creates a new MenuEntry, calling the constructor with the given parameters. /// /// The newly constructed MenuEntry. - protected virtual MenuEntry CreateMenuEntry(UnityAction action, string title, string description = null, - Color entryColor = default, bool enabled = true, Sprite icon = null) + protected virtual MenuEntry CreateMenuEntry(Action action, string title, string description = null, + Color entryColor = default, bool enabled = true, char icon = ' ') { - return new MenuEntry(action, null, title, description, entryColor, enabled, icon); - } - - [Test] - public void TestConstructorTitleNull() - { - Assert.Throws(() => _ = CreateMenuEntry(null, null)); - Assert.Throws(() => _ = CreateMenuEntry(() => { }, null)); + return new MenuEntry(action, title, null, description, entryColor, enabled, icon); } [Test] public void TestConstructorDefault() { - List testItems = new List(); + List testItems = new(); void Action() => testItems.Add(1); MenuEntry entry = CreateMenuEntry(Action, "Test"); Assert.AreEqual(null, entry.Description); Assert.AreEqual("Test", entry.Title); Assert.AreEqual(true, entry.Enabled); - Assert.AreEqual(null, entry.Icon); + Assert.AreEqual(' ', entry.Icon); Assert.AreEqual(default(Color), entry.EntryColor); #if INCLUDE_STEAM_VR @@ -72,8 +64,8 @@ public void TestConstructorDefault() } [Test, TestCaseSource(nameof(ValidConstructorSupplier))] - public void TestConstructor(UnityAction action, string title, string description, - Color entryColor, bool enabled, Sprite icon) + public void TestConstructor(Action action, string title, string description, + Color entryColor, bool enabled, char icon) { MenuEntry entry = CreateMenuEntry(action, title, description, entryColor, enabled, icon); // Given action must either be null or NOP for this test diff --git a/Assets/SEETests/UI/TestToggleMenuEntry.cs b/Assets/SEETests/UI/TestToggleMenuEntry.cs index 4171d5679e..1eaa91654c 100644 --- a/Assets/SEETests/UI/TestToggleMenuEntry.cs +++ b/Assets/SEETests/UI/TestToggleMenuEntry.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NUnit.Framework; using SEE.Utils; using UnityEngine; -using UnityEngine.Events; namespace SEE.UI.Menu { @@ -13,18 +13,18 @@ namespace SEE.UI.Menu [TestFixture] internal class TestToggleMenuEntry: TestMenuEntry { - protected override MenuEntry CreateMenuEntry(UnityAction action, string title, string description = null, + protected override MenuEntry CreateMenuEntry(Action action, string title, string description = null, Color entryColor = default, bool enabled = true, - Sprite icon = null) + char icon = '#') { - return new MenuEntry(action, null, title, description, entryColor, enabled, icon); + return new MenuEntry(action, title, null, description, entryColor, enabled, icon); } [Test] public void TestDefaultExitAction() { - MenuEntry entry1 = new( () => {}, null, "Test"); - MenuEntry entry2 = new( () => {}, null, "Test"); + MenuEntry entry1 = new(() => {}, "Test"); + MenuEntry entry2 = new(() => {}, "Test"); Assert.DoesNotThrow(() => entry1.SelectAction()); Assert.DoesNotThrow(() => entry2.SelectAction()); } @@ -38,7 +38,7 @@ public void TestExitAction() GameObject go = new("Test"); SelectionMenu selectionMenu = go.AddComponent(); void ExitAction() => testItems.Add(true); - MenuEntry entry = new(() => {}, ExitAction, "Test"); + MenuEntry entry = new(() => {}, "Test", ExitAction); selectionMenu.AddEntry(entry); Assert.AreNotEqual(entry, selectionMenu.ActiveEntry, "SelectionMenu.ActiveEntry isn't set correctly!"); Assert.AreEqual(0, testItems.Count, "Entry/ExitAction may not be called during initialization!"); diff --git a/Axivion/axivion-jenkins.bat b/Axivion/axivion-jenkins.bat index 1e3beb4a72..7b6845125f 100644 --- a/Axivion/axivion-jenkins.bat +++ b/Axivion/axivion-jenkins.bat @@ -107,7 +107,7 @@ if "%AXIVION_DASHBOARD_URL%"=="" ( ) if "%UNITY%"=="" ( - set "UNITY=C:\Program Files\Unity\Hub\Editor\2022.3.37f1" + set "UNITY=C:\Program Files\Unity\Hub\Editor\2022.3.40f1" ) if not exist "%UNITY%" ( diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index b12da398a4..cfbed01351 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2022.3.37f1 -m_EditorVersionWithRevision: 2022.3.37f1 (340ba89e4c23) +m_EditorVersion: 2022.3.40f1 +m_EditorVersionWithRevision: 2022.3.40f1 (cbdda657d2f0) diff --git a/README.md b/README.md index c7dad28611..71d7dcdbe7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Tests](https://github.com/uni-bremen-agst/SEE/actions/workflows/main.yml/badge.svg)](https://github.com/uni-bremen-agst/SEE/actions/workflows/main.yml) SEE visualizes hierarchical dependency graphs of software in 3D/VR based on the city metaphor. -The underlying game engine is Unity 3D (version 2022.3.37f1). +The underlying game engine is Unity 3D (version 2022.3.40f1). ![Screenshot of SEE](Screenshot.png)