├── .editorconfig ├── .github └── workflows │ ├── CI.yml │ └── PublishVSIX.yml ├── .gitignore ├── CHANGELOG.md ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE ├── NuGet.Config ├── README.md ├── THIRD-PARTY-NOTICES.txt ├── VSVersionSupport.md ├── marketplace ├── images │ ├── CommandWindow.png │ ├── ContextMenu.png │ ├── EditorGuidelines_128px.png │ ├── EditorGuidelines_90px.png │ └── FontsAndColors.png ├── overview.Dev17.md ├── overview.md ├── publishManifest.Dev17.json └── publishManifest.json ├── src ├── ColumnGuide │ ├── ColumnGuide.projitems │ ├── ColumnGuide.shproj │ ├── ColumnGuideAdornment.cs │ ├── ColumnGuideFactory.cs │ ├── EditorGuidelinesPackage.cs │ ├── EditorGuidelinesPackage.vsct │ ├── Guideline.cs │ ├── GuidelineBrush.cs │ ├── GuidelineColorDefinition.cs │ ├── HostServices.cs │ ├── ITextBufferExtensions.cs │ ├── ITextEditorGuidesSettings.cs │ ├── ITextEditorGuidesSettingsChanger.cs │ ├── IWpfTextViewExtensions.cs │ ├── LineStyle.cs │ ├── Parser.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Resources │ │ ├── Images_32bit.png │ │ └── Package.ico │ ├── StrokeParameters.cs │ ├── Telemetry.cs │ ├── TextEditorGuidesSettings.cs │ ├── TextEditorGuidesSettingsRendezvous.cs │ └── app.config ├── Editor Guidelines.sln ├── VSIX │ ├── CodingConventions.cs │ ├── VSIX.csproj │ └── source.extension.vsixmanifest └── VSIX_Dev17 │ ├── CodingConventions.cs │ ├── VSIX_Dev17.csproj │ └── source.extension.vsixmanifest └── test ├── ColumnGuideTests ├── ColumnGuideTests.csproj └── ParserTests.cs └── EditorGuidelinesTests ├── AbstractIntegrationTest.cs ├── EditorGuidelinesTests.csproj ├── Harness ├── AbstractServices.cs ├── KeyboardInput.cs ├── NativeMethods.cs ├── SendInputServices.cs ├── ShiftState.cs ├── SolutionServices.cs ├── TestServices.cs └── VisualStudioServices.cs ├── OpenFileTests.cs ├── Properties └── AssemblyInfo.cs └── Threading └── JoinableTaskFactoryExtensions.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs 2 | ############################### 3 | # Core EditorConfig Options # 4 | ############################### 5 | # All files 6 | [*] 7 | indent_style = space 8 | # Code files 9 | [*.{cs,csx,vb,vbx}] 10 | guidelines = 132 11 | guidelines_style = 2px dotted 40C00020 12 | indent_size = 4 13 | insert_final_newline = true 14 | charset = utf-8-bom 15 | ############################### 16 | # .NET Coding Conventions # 17 | ############################### 18 | [*.{cs,vb}] 19 | # Organize usings 20 | dotnet_sort_system_directives_first = true 21 | # this. preferences 22 | dotnet_style_qualification_for_field = false:silent 23 | dotnet_style_qualification_for_property = false:silent 24 | dotnet_style_qualification_for_method = false:silent 25 | dotnet_style_qualification_for_event = false:silent 26 | # Language keywords vs BCL types preferences 27 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 28 | dotnet_style_predefined_type_for_member_access = true:suggestion 29 | # Parentheses preferences 30 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion 31 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion 32 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion 33 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion 34 | # Modifier preferences 35 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 36 | dotnet_style_readonly_field = true:suggestion 37 | # Expression-level preferences 38 | dotnet_style_object_initializer = true:suggestion 39 | dotnet_style_collection_initializer = true:suggestion 40 | dotnet_style_explicit_tuple_names = true:suggestion 41 | dotnet_style_null_propagation = true:suggestion 42 | dotnet_style_coalesce_expression = true:suggestion 43 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 44 | dotnet_prefer_inferred_tuple_names = true:suggestion 45 | dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion 46 | dotnet_style_prefer_auto_properties = true:suggestion 47 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 48 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 49 | ############################### 50 | # Naming Conventions # 51 | ############################### 52 | # Style Definitions 53 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 54 | 55 | dotnet_naming_style.camel_case_with_leading_underscore.capitalization = camel_case 56 | dotnet_naming_style.camel_case_with_leading_underscore.required_prefix = _ 57 | 58 | dotnet_naming_style.camel_case_with_c_prefix_style.capitalization = camel_case 59 | dotnet_naming_style.camel_case_with_c_prefix_style.required_prefix = c_ 60 | 61 | dotnet_naming_style.camel_case_with_s_prefix.capitalization = camel_case 62 | dotnet_naming_style.camel_case_with_s_prefix.required_prefix = s_ 63 | 64 | # Symbol Definitions 65 | dotnet_naming_symbols.non_public_fields.applicable_kinds = field 66 | dotnet_naming_symbols.non_public_fields.applicable_accessibilities = private,protected,internal 67 | 68 | dotnet_naming_symbols.public_constant_fields.applicable_kinds = field 69 | dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public 70 | dotnet_naming_symbols.public_constant_fields.required_modifiers = const 71 | 72 | dotnet_naming_symbols.non_public_constant_fields.applicable_kinds = field 73 | dotnet_naming_symbols.non_public_constant_fields.applicable_accessibilities = private,protected,internal 74 | dotnet_naming_symbols.non_public_constant_fields.required_modifiers = const 75 | 76 | dotnet_naming_symbols.non_public_static_fields.applicable_kinds = field 77 | dotnet_naming_symbols.non_public_static_fields.applicable_accessibilities = private,protected,internal 78 | dotnet_naming_symbols.non_public_static_fields.required_modifiers = static 79 | 80 | # Use PascalCase for public constant fields 81 | dotnet_naming_rule.public_constant_fields_should_be_pascal_case.severity = suggestion 82 | dotnet_naming_rule.public_constant_fields_should_be_pascal_case.symbols = public_constant_fields 83 | dotnet_naming_rule.public_constant_fields_should_be_pascal_case.style = pascal_case_style 84 | 85 | # Use c_camelCase for non-public const fields 86 | dotnet_naming_rule.non_public_constant_fields_should_be_camel_case.severity = suggestion 87 | dotnet_naming_rule.non_public_constant_fields_should_be_camel_case.symbols = non_public_constant_fields 88 | dotnet_naming_rule.non_public_constant_fields_should_be_camel_case.style = camel_case_with_c_prefix_style 89 | 90 | # Use s_camelCase for non-public static fields 91 | dotnet_naming_rule.non_public_static_fields_should_be_c_camel_case.severity = suggestion 92 | dotnet_naming_rule.non_public_static_fields_should_be_c_camel_case.symbols = non_public_static_fields 93 | dotnet_naming_rule.non_public_static_fields_should_be_c_camel_case.style = camel_case_with_s_prefix 94 | 95 | # Use _camelCase for non-public fields 96 | dotnet_naming_rule.use_camel_case_with_leading_underscore_for_non_public_fields.severity = suggestion 97 | dotnet_naming_rule.use_camel_case_with_leading_underscore_for_non_public_fields.symbols = non_public_fields 98 | dotnet_naming_rule.use_camel_case_with_leading_underscore_for_non_public_fields.style = camel_case_with_leading_underscore 99 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 100 | tab_width = 4 101 | end_of_line = crlf 102 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 103 | ############################### 104 | # C# Coding Conventions # 105 | ############################### 106 | [*.cs] 107 | # var preferences 108 | csharp_style_var_for_built_in_types = true:silent 109 | csharp_style_var_when_type_is_apparent = true:suggestion 110 | csharp_style_var_elsewhere = true:silent 111 | # Expression-bodied members 112 | csharp_style_expression_bodied_methods = false:silent 113 | csharp_style_expression_bodied_constructors = false:silent 114 | csharp_style_expression_bodied_operators = false:silent 115 | csharp_style_expression_bodied_properties = true:silent 116 | csharp_style_expression_bodied_indexers = true:silent 117 | csharp_style_expression_bodied_accessors = true:silent 118 | # Pattern matching preferences 119 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 120 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 121 | # Null-checking preferences 122 | csharp_style_throw_expression = true:suggestion 123 | csharp_style_conditional_delegate_call = true:suggestion 124 | # Modifier preferences 125 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 126 | # Expression-level preferences 127 | csharp_prefer_braces = true:silent 128 | csharp_style_deconstructed_variable_declaration = true:suggestion 129 | csharp_prefer_simple_default_expression = true:suggestion 130 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 131 | csharp_style_inlined_variable_declaration = true:suggestion 132 | ############################### 133 | # C# Formatting Rules # 134 | ############################### 135 | # New line preferences 136 | csharp_new_line_before_open_brace = all 137 | csharp_new_line_before_else = true 138 | csharp_new_line_before_catch = true 139 | csharp_new_line_before_finally = true 140 | csharp_new_line_before_members_in_object_initializers = true 141 | csharp_new_line_before_members_in_anonymous_types = true 142 | csharp_new_line_between_query_expression_clauses = true 143 | # Indentation preferences 144 | csharp_indent_case_contents = true 145 | csharp_indent_switch_labels = true 146 | csharp_indent_labels = flush_left 147 | # Space preferences 148 | csharp_space_after_cast = false 149 | csharp_space_after_keywords_in_control_flow_statements = true 150 | csharp_space_between_method_call_parameter_list_parentheses = false 151 | csharp_space_between_method_declaration_parameter_list_parentheses = false 152 | csharp_space_between_parentheses = false 153 | csharp_space_before_colon_in_inheritance_clause = true 154 | csharp_space_after_colon_in_inheritance_clause = true 155 | csharp_space_around_binary_operators = before_and_after 156 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 157 | csharp_space_between_method_call_name_and_opening_parenthesis = false 158 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 159 | # Wrapping preferences 160 | csharp_preserve_single_line_statements = true 161 | csharp_preserve_single_line_blocks = true 162 | csharp_using_directive_placement = outside_namespace:silent 163 | csharp_prefer_simple_using_statement = true:suggestion 164 | csharp_style_namespace_declarations = block_scoped:silent 165 | csharp_style_prefer_method_group_conversion = true:silent 166 | csharp_style_prefer_top_level_statements = true:silent 167 | csharp_style_prefer_primary_constructors = true:suggestion 168 | csharp_style_expression_bodied_lambdas = true:silent 169 | csharp_style_expression_bodied_local_functions = false:silent 170 | ############################### 171 | # VB Coding Conventions # 172 | ############################### 173 | [*.vb] 174 | # Modifier preferences 175 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion 176 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | strategy: 14 | matrix: 15 | configuration: [Debug, Release] 16 | 17 | runs-on: windows-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4.1.1 22 | with: 23 | fetch-depth: 0 24 | 25 | # Install the .NET Core workload 26 | - name: Install .NET Core 27 | uses: actions/setup-dotnet@v4.0.0 28 | with: 29 | dotnet-version: 8.0.x 30 | 31 | # Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild 32 | - name: Setup MSBuild.exe 33 | uses: microsoft/setup-msbuild@v1.3.2 34 | 35 | # Restore and build 36 | - name: Restore and build 37 | run: msbuild src /t:Restore,Build /p:Configuration=$env:Configuration /p:DeployExtension=false 38 | env: 39 | Configuration: ${{ matrix.configuration }} 40 | 41 | # Run unit tests 42 | - name: Run unit tests 43 | run: dotnet test --no-build test\ColumnGuideTests\ColumnGuideTests.csproj --configuration $env:Configuration 44 | env: 45 | Configuration: ${{ matrix.configuration }} 46 | 47 | # Upload the VSIX package: https://github.com/marketplace/actions/upload-a-build-artifact 48 | - name: Upload build artifact (VSIX) 49 | uses: actions/upload-artifact@v4.3.0 50 | with: 51 | name: VSIX Package ${{ matrix.Configuration }} 52 | path: src/VSIX/bin/**/*.vsix 53 | 54 | - name: Upload build artifact (Dev 17 VSIX) 55 | uses: actions/upload-artifact@v4.3.0 56 | with: 57 | name: VSIX Package ${{ matrix.Configuration }} Dev 17 58 | path: src/VSIX_Dev17/bin/**/*.vsix 59 | -------------------------------------------------------------------------------- /.github/workflows/PublishVSIX.yml: -------------------------------------------------------------------------------- 1 | name: Publish to VS Marketplace 2 | 3 | # Triggered by a new GitHub Release being published. It expects to find 4 | # two .visx assets in the Release: 5 | # EditorGuidelines.vsix and 6 | # EditorGuidelines.Dev17.vsix 7 | 8 | on: 9 | release: 10 | types: [published] 11 | # workflow_dispatch: 12 | 13 | jobs: 14 | publish: 15 | runs-on: windows-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4.1.1 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Fetch latest release assets 23 | uses: dsaltares/fetch-gh-release-asset@1.1.1 24 | with: 25 | file: EditorGuidelines.vsix 26 | 27 | - name: Publish to VS marketplace 28 | uses: CalvinAllen/action-vs-marketplace-publish@v1 29 | with: 30 | marketplace-pat: ${{ secrets.vs_pat }} 31 | publish-manifest-path: marketplace/publishManifest.json 32 | vsix-path: EditorGuidelines.vsix 33 | 34 | - name: Fetch latest release assets 35 | uses: dsaltares/fetch-gh-release-asset@1.1.1 36 | with: 37 | file: EditorGuidelines.Dev17.vsix 38 | 39 | - name: Publish Dev17 to marketplace 40 | uses: CalvinAllen/action-vs-marketplace-publish@v1 41 | with: 42 | marketplace-pat: ${{ secrets.vs_pat }} 43 | publish-manifest-path: marketplace/publishManifest.Dev17.json 44 | vsix-path: EditorGuidelines.Dev17.vsix 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opendb 78 | *.opensdf 79 | *.sdf 80 | *.cachefile 81 | *.VC.db 82 | *.VC.VC.opendb 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | *.sap 89 | 90 | # TFS 2012 Local Workspace 91 | $tf/ 92 | 93 | # Guidance Automation Toolkit 94 | *.gpState 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding add-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # Visual Studio code coverage results 111 | *.coverage 112 | *.coveragexml 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | 119 | # MightyMoose 120 | *.mm.* 121 | AutoTest.Net/ 122 | 123 | # Web workbench (sass) 124 | .sass-cache/ 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | *.[Pp]ublish.xml 144 | *.azurePubxml 145 | # TODO: Comment the next line if you want to checkin your web deploy settings 146 | # but database connection strings (with potential passwords) will be unencrypted 147 | *.pubxml 148 | *.publishproj 149 | 150 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 151 | # checkin your Azure Web App publish settings, but sensitive information contained 152 | # in these scripts will be unencrypted 153 | PublishScripts/ 154 | 155 | # NuGet Packages 156 | *.nupkg 157 | # The packages folder can be ignored because of Package Restore 158 | **/packages/* 159 | # except build/, which is used as an MSBuild target. 160 | !**/packages/build/ 161 | # Uncomment if necessary however generally it will be regenerated when needed 162 | #!**/packages/repositories.config 163 | # NuGet v3's project.json files produces more ignorable files 164 | *.nuget.props 165 | *.nuget.targets 166 | 167 | # Microsoft Azure Build Output 168 | csx/ 169 | *.build.csdef 170 | 171 | # Microsoft Azure Emulator 172 | ecf/ 173 | rcf/ 174 | 175 | # Windows Store app package directories and files 176 | AppPackages/ 177 | BundleArtifacts/ 178 | Package.StoreAssociation.xml 179 | _pkginfo.txt 180 | 181 | # Visual Studio cache files 182 | # files ending in .cache can be ignored 183 | *.[Cc]ache 184 | # but keep track of directories ending in .cache 185 | !*.[Cc]ache/ 186 | 187 | # Others 188 | ClientBin/ 189 | ~$* 190 | *~ 191 | *.dbmdl 192 | *.dbproj.schemaview 193 | *.jfm 194 | *.pfx 195 | *.publishsettings 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | *.ndf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | node_modules/ 232 | 233 | # Typescript v1 declaration files 234 | typings/ 235 | 236 | # Visual Studio 6 build log 237 | *.plg 238 | 239 | # Visual Studio 6 workspace options file 240 | *.opt 241 | 242 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 243 | *.vbw 244 | 245 | # Visual Studio LightSwitch build output 246 | **/*.HTMLClient/GeneratedArtifacts 247 | **/*.DesktopClient/GeneratedArtifacts 248 | **/*.DesktopClient/ModelManifest.xml 249 | **/*.Server/GeneratedArtifacts 250 | **/*.Server/ModelManifest.xml 251 | _Pvt_Extensions 252 | 253 | # Paket dependency manager 254 | .paket/paket.exe 255 | paket-files/ 256 | 257 | # FAKE - F# Make 258 | .fake/ 259 | 260 | # JetBrains Rider 261 | .idea/ 262 | *.sln.iml 263 | 264 | # CodeRush 265 | .cr/ 266 | 267 | # Python Tools for Visual Studio (PTVS) 268 | __pycache__/ 269 | *.pyc 270 | 271 | # Cake - Uncomment if you are using it 272 | # tools/** 273 | # !tools/packages.config 274 | 275 | # Telerik's JustMock configuration file 276 | *.jmconfig 277 | 278 | # BizTalk build output 279 | *.btp.cs 280 | *.btm.cs 281 | *.odx.cs 282 | *.xsd.cs 283 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Editor Guidelines 2 | All notable changes will be documented in this file. 3 | 4 | ## Version [2.2.11] (January 26th 2024) 5 | ### Fixed 6 | - Fixed issue [#122](https://github.com/pharring/EditorGuidelines/issues/122) where Editor Guidelines failed to install on Visual Studio 2022 (17.10 preview) due to a missing CodingConventions package. 7 | 8 | ## Version [2.2.10] (April 24th 2023) 9 | ### Fixed 10 | - Fixed issue [#116](https://github.com/pharring/EditorGuidelines/issues/116) where Editor Guidelines broke the Visual Studio Spell Checker in Visual Studio 2022 (17.5.4) 11 | 12 | ## Version [2.2.9] (Dec 31st 2022) 13 | ### Added 14 | - Support for ARM in the VS 2022 version. Issue [#106](https://github.com/pharring/EditorGuidelines/issues/106) 15 | 16 | ## Version [2.2.8] (Nov 15th 2021) 17 | ### Changed 18 | - Removed the Preview tag from the VS 2022 version. 19 | 20 | ## Version [2.2.7] (June 21st 2021) 21 | ### Fixed 22 | - Fixed issue [#82](https://github.com/pharring/EditorGuidelines/issues/82) where version 2.2.6 wouldn't load in VS 2015 and VS 2017. 23 | 24 | ## Version [2.2.6] (June 19th 2021) 25 | ### Removed 26 | - Support for VS 2012 and VS 2013 27 | 28 | ## Version [2.2.5] (March 26th 2020) 29 | ### 30 | - Last version to support VS 2012 and VS 2013 31 | 32 | ### Added 33 | - Support for different style for each guideline specified in .editorconfig 34 | 35 | ## Version [2.2.4] (June 14th 2019) 36 | ### Changed 37 | - Updated context menu icons. Issue [#49](https://github.com/pharring/EditorGuidelines/issues/49) 38 | 39 | ## Version [2.2.3] (April 28th 2019) 40 | ### Added 41 | - Line width set in guidelines_style can now be non-integral (e.g. 0.5px) 42 | 43 | ### Fixed 44 | - Fixed issue [#37](https://github.com/pharring/EditorGuidelines/issues/37) where you couldn't set guidelines via .editorconfig in HTML-based files. 45 | 46 | ## Version [2.2.2] (April 21st 2019) 47 | ### Added 48 | - Style and color may be set via guidelines_style setting in .editorconfig 49 | 50 | ## Version [2.2.1] (April 7th 2019) 51 | ### Added 52 | - CD release to Marketplace from Azure Dev Ops 53 | 54 | ## Version [2.2.0] (April 7th 2019) 55 | ### Added 56 | - Support for max_line_length in .editorconfig 57 | 58 | ## Version [2.1.0] (December 12th 2018) 59 | ### Added 60 | - .editorconfig support (VS 2017 and VS 2019 only) 61 | 62 | ## Version [2.0.4] (December 7th 2018) 63 | ### Added 64 | - Support for Visual Studio 2019 (Dev16). 65 | 66 | ### Changed 67 | - Thinned out telemetry events. 68 | 69 | ## Version [2.0.3] (December 26th 2017) 70 | ### Changed 71 | - Updated icon with one from the PPT artwork team. 72 | 73 | ## Version [2.0.2] (December 24th 2017) 74 | ### Changed 75 | - Update icon again. 76 | 77 | ## Version [2.0.1] (December 24th 2017) 78 | ### Changed 79 | - Update icon (with permission from PPT team). 80 | 81 | ## Version [2.0.0] (December 23rd 2017) 82 | ### Changed 83 | - MIT license. 84 | - Open sourced on https://github.com/pharring/EditorGuidelines 85 | 86 | ## Version 1.15.61202.0 (December 2nd 2016) 87 | ### Changed 88 | - Thinned out telemetry events. 89 | 90 | ## Version 1.15.61129.0 (November 29th 2016) 91 | ### Added 92 | - Moved color selection into 'Fonts and Colors' section in Tools/Options. 93 | 94 | ## Version 1.15.61103.1 (November 3rd 2016) 95 | ### Added 96 | - Usage telemetry 97 | 98 | ## Version 1.15.61102.0 (November 2nd 2016) 99 | ### Added 100 | - Support VS "15" RC 101 | 102 | ## Version 1.11.70722.0 (July 22nd 2015) 103 | ### Added 104 | - "Remove All Guidelines" command. 105 | - "Edit.AddGuideline" and "Edit.RemoveGuideline" can now take a parameter when invoked from the command window indicating which column to add or remove. 106 | 107 | ### Fixed 108 | - Fixed a bug where the guideline menu was missing from context menu in HTML files in VS 2013. 109 | 110 | ### Changed 111 | - Updated description to indicate that VS 2015 is supported. 112 | 113 | [2.2.11]: https://github.com/pharring/EditorGuidelines/compare/2.2.9..2.2.11 114 | [2.2.10]: https://github.com/pharring/EditorGuidelines/compare/2.2.9..2.2.10 115 | [2.2.9]: https://github.com/pharring/EditorGuidelines/compare/2.2.8..2.2.9 116 | [2.2.8]: https://github.com/pharring/EditorGuidelines/compare/2.2.7..2.2.8 117 | [2.2.7]: https://github.com/pharring/EditorGuidelines/compare/2.2.6..2.2.7 118 | [2.2.6]: https://github.com/pharring/EditorGuidelines/compare/2.2.5..2.2.6 119 | [2.2.5]: https://github.com/pharring/EditorGuidelines/compare/2.2.4..2.2.5 120 | [2.2.4]: https://github.com/pharring/EditorGuidelines/compare/2.2.3..2.2.4 121 | [2.2.3]: https://github.com/pharring/EditorGuidelines/compare/2.2.2..2.2.3 122 | [2.2.2]: https://github.com/pharring/EditorGuidelines/compare/2.2.1..2.2.2 123 | [2.2.1]: https://github.com/pharring/EditorGuidelines/compare/2.2.0..2.2.1 124 | [2.2.0]: https://github.com/pharring/EditorGuidelines/compare/2.1.0..2.2.0 125 | [2.1.0]: https://github.com/pharring/EditorGuidelines/compare/2.0.4..2.1.0 126 | [2.0.4]: https://github.com/pharring/EditorGuidelines/compare/v2.0.3..2.0.4 127 | [2.0.3]: https://github.com/pharring/EditorGuidelines/compare/v2.0.2..v2.0.3 128 | [2.0.2]: https://github.com/pharring/EditorGuidelines/compare/v2.0.1..v2.0.2 129 | [2.0.1]: https://github.com/pharring/EditorGuidelines/compare/v2.0.0..v2.0.1 130 | [2.0.0]: https://github.com/pharring/EditorGuidelines/releases/tag/v2.0.0 131 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Paul Harrington 6 | Copyright © 2024 Paul Harrington 7 | 2.2.11 8 | 7.3 9 | true 10 | $(MSBuildThisFileDirectory) 11 | $([MSBuild]::MakeRelative($(EnlistmentRoot), $(MSBuildProjectDirectory)).StartsWith('test\')) 12 | true 13 | 14 | 15 | 16 | true 17 | full 18 | 19 | 20 | 21 | true 22 | pdbonly 23 | 24 | 25 | 26 | false 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <_PackageVersion>$(PackageVersion) 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Paul Harrington 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Editor Guidelines 2 | A Visual Studio extension that adds vertical column guides to the text editor. 3 | 4 | [![Build Status](https://github.com/pharring/EditorGuidelines/actions/workflows/CI.yml/badge.svg)](https://github.com/pharring/EditorGuidelines/actions/workflows/CI.yml) 5 | 6 | The extension adds vertical column guides behind your code. This is useful if you are trying to tabulate columns of data or if you want to ensure that your lines don't extend beyond a certain length. You specify where the guides go and what color they should be. 7 | 8 | #### Visual Studio 2022 9 | [![Visual Studio Marketplace](https://vsmarketplacebadges.dev/version-short/PaulHarrington.EditorGuidelinesPreview.svg)](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview) 10 | [![Visual Studio Marketplace Rating](https://vsmarketplacebadges.dev/rating-star/PaulHarrington.EditorGuidelinesPreview.svg)](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview) 11 | [![Visual Studio Marketplace Downloads](https://vsmarketplacebadges.dev/downloads-short/PaulHarrington.EditorGuidelinesPreview.svg)](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview) 12 | 13 | #### Visual Studio 2015, 2017, 2019 14 | [![Visual Studio Marketplace](https://vsmarketplacebadges.dev/version-short/PaulHarrington.EditorGuidelines.svg)](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines) 15 | [![Visual Studio Marketplace Rating](https://vsmarketplacebadges.dev/rating-star/PaulHarrington.EditorGuidelines.svg)](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines) 16 | [![Visual Studio Marketplace Downloads](https://vsmarketplacebadges.dev/downloads-short/PaulHarrington.EditorGuidelines.svg)](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines) 17 | 18 | ## Getting Started 19 | 20 | You can either download the extension (VSIX) from the Visual Studio Marketplace and manually install it, or intall it from within Visual Studio itself. 21 | 22 | ### Download from the Visual Studio Marketplace 23 | There are two versions in the Visual Studio Marketplace 24 | - [Download for Visual Studio 2022](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview) 25 | - [Download for Visual Studio 2015, 2017, 2019](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines) 26 | 27 | Once downloaded, double-click on the downloaded file (.VSIX) and follow the prompts to install it into Visual Studio. 28 | 29 | ### Install from within Visual Studio 30 | - Visual Studio 2019 and 2022: Select "Manage Extensions" on the "Extensions" menu. Search for "Editor Guidelines" in the Visual Studio Marketplace. 31 | - Visual Studio 2015 and 2017: Select "Extensions and Updates..." from the "Tools" menu. Search for "Editor Guidelines" in the Visual Studio Marketplace. 32 | 33 | You will then have to close and restart Visual Studio for the extension to be fully installed. 34 | 35 | ## Usage 36 | Control guidelines via the context (right-click) menu on the editor surface. You will see a *Guidelines* flyout with three commands: 37 | 38 | ![GuidelinesContextMenu](marketplace/images/ContextMenu.png) 39 | 40 | * When *Add Guideline* is selected, a vertical dashed line will be drawn at the same position as the caret (insertion point). 41 | * *Remove Guideline* will remove any guideline at the current insertion point. 42 | * *Remove All Guidelines* does exactly that. 43 | 44 | These commands may also be accessed from Visual Studio's Command Window. 45 | 46 | ![GuidelinesCommandWindow](marketplace/images/CommandWindow.png) 47 | 48 | Note that the column numbers used for the `Edit.AddGuideline` and `Edit.RemoveGuideline` commands refer to the right side of the given column of text. 49 | i.e. To place a guide to the right of column 80, use `Edit.AddGuideline 80`. To place a guide to the left of the first column use `Edit.AddGuideline 0`. 50 | 51 | You can change the guideline color from the Fonts and Colors page in `Tools|Options`. Look for *Guideline* in the Text Editor category: 52 | 53 | ![GuidelinesToolsOptions](marketplace/images/FontsAndColors.png) 54 | 55 | ## .editorconfig support (VS 2017 or above) 56 | For VS 2017, VS 2019 and VS 2022, the position of guidelines can be overridden via settings in .editorconfig files. 57 | Set the `guidelines` property to a list of column values. The following example sets guidelines at columns 80 and 120 for C# and VB files and a single guideline at column 80 for all other files. 58 | 59 | ```ini 60 | # All files 61 | [*] 62 | guidelines = 80 63 | 64 | # C# or VB files 65 | [*.{cs,vb}] 66 | guidelines = 80, 120 67 | ``` 68 | 69 | You can set the guideline style like this: 70 | ```ini 71 | [*] 72 | # Named color format 73 | guidelines_style = 1px dotted black 74 | 75 | [*.{cs,vb}] 76 | # ARGB color format (red with 25% opacity) 77 | guidelines_style = 2.5px solid 40ff0000 78 | ``` 79 | As shown, you can have different styles for different file types. There are three different drawing styles: 80 | - solid 81 | - dotted 82 | - dashed 83 | 84 | As the examples show, colors may be named or in RGB or ARGB (hexadecimal) format. The available color names are from WPF's Colors collection (System.Windows.Media.Colors). 85 | 86 | As the following example shows, you can set the style for each guideline separately. Three guidelines are defined. The first two define custom styles. The third, at column 132 doesn't specify a style, so it will be drawn using the default style which, if not specified via `guidelines_style`, will take its color from Fonts & Colors. 87 | 88 | ```ini 89 | [*] 90 | guidelines = 40 1px dotted black, 80 10px solid 30B0ED4C, 132 91 | ``` 92 | 93 | To learn more about .editorconfig see https://aka.ms/editorconfigdocs 94 | 95 | **Note:** When guidelines are set via .editorconfig they override any other guidelines set via the context menus or command window. 96 | 97 | _Note: This extension collects and transmits anonymized usage statistics to the extension author for product improvement purposes._ 98 | -------------------------------------------------------------------------------- /THIRD-PARTY-NOTICES.txt: -------------------------------------------------------------------------------- 1 | Editor Guidelines uses third-party libraries or other resources that may be 2 | distributed under licenses different than the Editor Guidelines software. 3 | 4 | In the event that we accidentally failed to list a required notice, please 5 | bring it to our attention by posting an issue. 6 | 7 | The attached notices are provided for information only. 8 | 9 | License notice for .NET Compiler Platform (Roslyn) 10 | -------------------------------------------------- 11 | 12 | Copyright Microsoft. 13 | 14 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 15 | these files except in compliance with the License. You may obtain a copy of the 16 | License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unless required by applicable law or agreed to in writing, software distributed 21 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 22 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 23 | specific language governing permissions and limitations under the License. 24 | 25 | License notice for Microsoft.VisualStudio.Threading 26 | --------------------------------------------------- 27 | 28 | Microsoft.VisualStudio.Threading 29 | Copyright (c) Microsoft Corporation 30 | All rights reserved. 31 | 32 | MIT License 33 | 34 | Permission is hereby granted, free of charge, to any person obtaining a copy 35 | of this software and associated documentation files (the "Software"), to deal 36 | in the Software without restriction, including without limitation the rights 37 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 38 | copies of the Software, and to permit persons to whom the Software is 39 | furnished to do so, subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in all 42 | copies or substantial portions of the Software. 43 | 44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 45 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 46 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 47 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 48 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 49 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 50 | SOFTWARE. 51 | -------------------------------------------------------------------------------- /VSVersionSupport.md: -------------------------------------------------------------------------------- 1 | # Editor Guidelines support for older Visual Studio versions. 2 | 3 | Version 2.2.5 of Editor Guidelines will be the last version with support for Visual Studio 2012 and 2013. 4 | Future versions will require Visual Studio 2015 or above. 5 | For continued support of older versions we will publish an archived version of Editor Guidelines under a new name. When that extension is available, we'll provide a link here. -------------------------------------------------------------------------------- /marketplace/images/CommandWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharring/EditorGuidelines/26d146201a3d56b3f741cf860b5eea44cb42e726/marketplace/images/CommandWindow.png -------------------------------------------------------------------------------- /marketplace/images/ContextMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharring/EditorGuidelines/26d146201a3d56b3f741cf860b5eea44cb42e726/marketplace/images/ContextMenu.png -------------------------------------------------------------------------------- /marketplace/images/EditorGuidelines_128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharring/EditorGuidelines/26d146201a3d56b3f741cf860b5eea44cb42e726/marketplace/images/EditorGuidelines_128px.png -------------------------------------------------------------------------------- /marketplace/images/EditorGuidelines_90px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharring/EditorGuidelines/26d146201a3d56b3f741cf860b5eea44cb42e726/marketplace/images/EditorGuidelines_90px.png -------------------------------------------------------------------------------- /marketplace/images/FontsAndColors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharring/EditorGuidelines/26d146201a3d56b3f741cf860b5eea44cb42e726/marketplace/images/FontsAndColors.png -------------------------------------------------------------------------------- /marketplace/overview.Dev17.md: -------------------------------------------------------------------------------- 1 | Editor Guidelines adds vertical column guides behind your code. This is useful if you are trying to tabulate columns of data or if you want to ensure that your lines don't extend beyond a certain length. You specify where the guides go and what color they should be. 2 | 3 | _Note: This version is specifically for Visual Studio 2022._ 4 | 5 | _Note: This extension collects and transmits anonymized usage statistics to the extension author for product improvement purposes._ 6 | 7 | ## Getting Started 8 | Add a new guideline via the context (right-click) menu on the editor surface. You will see a "Guidelines" flyout with three commands: 9 | 10 | ![Context Menu](images/ContextMenu.png) 11 | 12 | When "Add Guideline" is selected, a vertical dashed line will be drawn at the same position as the caret (insertion point). 13 | 14 | "Remove Guideline" will remove any guideline at the current insertion point. 15 | 16 | "Remove All Guidelines" does exactly that. 17 | 18 | ## Configuration 19 | You can change the guideline color from the Fonts and Colors page in Tools/Options. Look for "Guideline" in the Text Editor category: 20 | 21 | ![Fonts and Colors](images/FontsAndColors.png) 22 | 23 | ## .editorconfig support (VS 2017 and above) 24 | For VS 2017 and above, the position of guidelines can be overridden via settings in .editorconfig files. 25 | Set the `guidelines` property to a list of column values. The following example sets guidelines at columns 80 and 120 for C# and VB files and a single guideline at column 80 for all other files. 26 | 27 | ```ini 28 | # All files 29 | [*] 30 | guidelines = 80 31 | 32 | # C# or VB files 33 | [*.{cs,vb}] 34 | guidelines = 80, 120 35 | ``` 36 | 37 | You can set the guideline style like this: 38 | ```ini 39 | [*] 40 | # Named color format 41 | guidelines_style = 1px dotted black 42 | 43 | [*.{cs,vb}] 44 | # ARGB color format (red with 25% opacity) 45 | guidelines_style = 2.5px solid 40ff0000 46 | ``` 47 | As shown, you can have different styles for different file types. There are three different drawing styles: 48 | - solid 49 | - dotted 50 | - dashed 51 | 52 | As the examples show, colors may be named or in RGB or ARGB (hexadecimal) format. The available color names are from WPF's Colors collection (System.Windows.Media.Colors). 53 | 54 | As the following example shows, you can set the style for each guideline separately. Three guidelines are defined. The first two define custom styles. The third, at column 132 doesn't specify a style, so it will be drawn using the default style which, if not specified via `guidelines_style`, will take its color from Fonts & Colors. 55 | 56 | ```ini 57 | [*] 58 | guidelines = 40 1px dotted black, 80 10px solid 30B0ED4C, 132 59 | ``` 60 | 61 | To learn more about .editorconfig see https://aka.ms/editorconfigdocs 62 | 63 | **Note:** When guidelines are set via .editorconfig they override any other guidelines set via the context menus or command window. 64 | 65 | ## Advanced 66 | These commands may also be accessed from Visual Studio's Command Window 67 | 68 | ![Command Window](images/CommandWindow.png) 69 | 70 | Note that the column numbers used for the Edit.AddGuideline and Edit.RemoveGuideline commands refer to the right side of the given column of text. i.e. To place a guide to the right of column 80, use "Edit.AddGuideline 80". To place a guide to the left of the first column use "Edit.AddGuideline 0". 71 | 72 | ## Support 73 | If you find a bug in this extension or have a feature request, please visit https://github.com/pharring/EditorGuidelines to file an issue. 74 | -------------------------------------------------------------------------------- /marketplace/overview.md: -------------------------------------------------------------------------------- 1 | Editor Guidelines adds vertical column guides behind your code. This is useful if you are trying to tabulate columns of data or if you want to ensure that your lines don't extend beyond a certain length. You specify where the guides go and what color they should be. 2 | 3 | Looking for a version that works with **Visual Studio 2022**? Please [go here](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview) to download that version. 4 | 5 | _Note: This extension collects and transmits anonymized usage statistics to the extension author for product improvement purposes._ 6 | 7 | ## Getting Started 8 | Add a new guideline via the context (right-click) menu on the editor surface. You will see a "Guidelines" flyout with three commands: 9 | 10 | ![Context Menu](images/ContextMenu.png) 11 | 12 | When "Add Guideline" is selected, a vertical dashed line will be drawn at the same position as the caret (insertion point). 13 | 14 | "Remove Guideline" will remove any guideline at the current insertion point. 15 | 16 | "Remove All Guidelines" does exactly that. 17 | 18 | ## Configuration 19 | You can change the guideline color from the Fonts and Colors page in Tools/Options. Look for "Guideline" in the Text Editor category: 20 | 21 | ![Fonts and Colors](images/FontsAndColors.png) 22 | 23 | ## .editorconfig support (VS 2017 and above) 24 | For VS 2017 and above, the position of guidelines can be overridden via settings in .editorconfig files. 25 | Set the `guidelines` property to a list of column values. The following example sets guidelines at columns 80 and 120 for C# and VB files and a single guideline at column 80 for all other files. 26 | 27 | ```ini 28 | # All files 29 | [*] 30 | guidelines = 80 31 | 32 | # C# or VB files 33 | [*.{cs,vb}] 34 | guidelines = 80, 120 35 | ``` 36 | 37 | You can set the guideline style like this: 38 | ```ini 39 | [*] 40 | # Named color format 41 | guidelines_style = 1px dotted black 42 | 43 | [*.{cs,vb}] 44 | # ARGB color format (red with 25% opacity) 45 | guidelines_style = 2.5px solid 40ff0000 46 | ``` 47 | As shown, you can have different styles for different file types. There are three different drawing styles: 48 | - solid 49 | - dotted 50 | - dashed 51 | 52 | As the examples show, colors may be named or in RGB or ARGB (hexadecimal) format. The available color names are from WPF's Colors collection (System.Windows.Media.Colors). 53 | 54 | As the following example shows, you can set the style for each guideline separately. Three guidelines are defined. The first two define custom styles. The third, at column 132 doesn't specify a style, so it will be drawn using the default style which, if not specified via `guidelines_style`, will take its color from Fonts & Colors. 55 | 56 | ```ini 57 | [*] 58 | guidelines = 40 1px dotted black, 80 10px solid 30B0ED4C, 132 59 | ``` 60 | 61 | To learn more about .editorconfig see https://aka.ms/editorconfigdocs 62 | 63 | **Note:** When guidelines are set via .editorconfig they override any other guidelines set via the context menus or command window. 64 | 65 | ## Advanced 66 | These commands may also be accessed from Visual Studio's Command Window 67 | 68 | ![Command Window](images/CommandWindow.png) 69 | 70 | Note that the column numbers used for the Edit.AddGuideline and Edit.RemoveGuideline commands refer to the right side of the given column of text. i.e. To place a guide to the right of column 80, use "Edit.AddGuideline 80". To place a guide to the left of the first column use "Edit.AddGuideline 0". 71 | 72 | ## Support 73 | If you find a bug in this extension or have a feature request, please visit https://github.com/pharring/EditorGuidelines to file an issue. 74 | -------------------------------------------------------------------------------- /marketplace/publishManifest.Dev17.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/vsix-publish", 3 | "categories": [ "coding", "other" ], 4 | "identity": { 5 | "internalName": "EditorGuidelinesPreview" 6 | }, 7 | "overview": "overview.Dev17.md", 8 | "priceCategory": "free", 9 | "publisher": "PaulHarrington", 10 | "private": false, 11 | "qna": true, 12 | "repo": "https://github.com/pharring/EditorGuidelines", 13 | "assetFiles": [ 14 | { 15 | "pathOnDisk": "images/ContextMenu.png", 16 | "targetPath": "images/ContextMenu.png" 17 | }, 18 | { 19 | "pathOnDisk": "images/FontsAndColors.png", 20 | "targetPath": "images/FontsAndColors.png" 21 | }, 22 | { 23 | "pathOnDisk": "images/CommandWindow.png", 24 | "targetPath": "images/CommandWindow.png" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /marketplace/publishManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/vsix-publish", 3 | "categories": [ "coding", "other" ], 4 | "identity": { 5 | "internalName": "EditorGuidelines" 6 | }, 7 | "overview": "overview.md", 8 | "priceCategory": "free", 9 | "publisher": "PaulHarrington", 10 | "private": false, 11 | "qna": true, 12 | "repo": "https://github.com/pharring/EditorGuidelines", 13 | "assetFiles": [ 14 | { 15 | "pathOnDisk": "images/ContextMenu.png", 16 | "targetPath": "images/ContextMenu.png" 17 | }, 18 | { 19 | "pathOnDisk": "images/FontsAndColors.png", 20 | "targetPath": "images/FontsAndColors.png" 21 | }, 22 | { 23 | "pathOnDisk": "images/CommandWindow.png", 24 | "targetPath": "images/CommandWindow.png" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /src/ColumnGuide/ColumnGuide.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | 61d4b2d7-4433-4c71-be24-57a36758ed99 7 | 8 | 9 | EditorGuidelines 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Resources.resx 28 | True 29 | True 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Resources.Designer.cs 49 | ResXFileCodeGenerator 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/ColumnGuide/ColumnGuide.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 61d4b2d7-4433-4c71-be24-57a36758ed99 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ColumnGuide/ColumnGuideAdornment.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Microsoft.ApplicationInsights.DataContracts; 4 | using Microsoft.VisualStudio.Text.Editor; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.ComponentModel; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using System.Windows; 12 | using System.Windows.Media; 13 | using System.Windows.Shapes; 14 | using static EditorGuidelines.Parser; 15 | 16 | namespace EditorGuidelines 17 | { 18 | /// 19 | /// Adornment class that draws vertical guide lines beneath the text 20 | /// 21 | internal class ColumnGuideAdornment : IDisposable 22 | { 23 | /// 24 | /// Limits the number of telemetry events for .editorconfig settings. 25 | /// 26 | private static bool s_sentEditorConfigTelemetry; 27 | 28 | /// 29 | /// Collection of WPF lines that are drawn into our adornment. 30 | /// 31 | private IEnumerable _lines; 32 | 33 | /// 34 | /// The WPF text view associated with this adornment. 35 | /// 36 | private readonly IWpfTextView _view; 37 | 38 | /// 39 | /// True when the first layout has completed. 40 | /// 41 | private bool _firstLayoutDone; 42 | 43 | private double _baseIndentation; 44 | private double _columnWidth; 45 | 46 | private INotifyPropertyChanged _settingsChanged; 47 | 48 | /// 49 | /// The brush supplied by Fonts and Colors 50 | /// 51 | private GuidelineBrush _guidelineBrush; 52 | 53 | /// 54 | /// The stroke parameters for the . 55 | /// 56 | private readonly StrokeParameters _strokeParameters; 57 | 58 | private readonly CancellationTokenSource _codingConventionsCancellationTokenSource; 59 | private bool _isUsingCodingConvention; 60 | 61 | /// 62 | /// Creates editor column guidelines 63 | /// 64 | /// The upon which the adornment will be drawn 65 | /// The guideline settings. 66 | /// The guideline brush. 67 | /// The coding conventions manager for handling .editorconfig settings. 68 | public ColumnGuideAdornment(IWpfTextView view, ITextEditorGuidesSettings settings, GuidelineBrush guidelineBrush, CodingConventions codingConventions) 69 | { 70 | _view = view; 71 | _guidelineBrush = guidelineBrush; 72 | _guidelineBrush.BrushChanged += GuidelineBrushChanged; 73 | _strokeParameters = StrokeParameters.FromBrush(_guidelineBrush.Brush); 74 | 75 | if (codingConventions != null) 76 | { 77 | _codingConventionsCancellationTokenSource = new CancellationTokenSource(); 78 | var fireAndForgetTask = LoadGuidelinesFromEditorConfigAsync(codingConventions, view); 79 | } 80 | 81 | InitializeGuidelines(settings.GuideLinePositionsInChars); 82 | 83 | _view.LayoutChanged += OnViewLayoutChanged; 84 | _settingsChanged = settings as INotifyPropertyChanged; 85 | if (_settingsChanged != null) 86 | { 87 | _settingsChanged.PropertyChanged += SettingsChanged; 88 | } 89 | 90 | _view.Closed += ViewClosed; 91 | } 92 | 93 | private void GuidelineBrushChanged(object sender, Brush brush) 94 | { 95 | _strokeParameters.Brush = brush; 96 | if (_lines != null) 97 | { 98 | foreach (var line in _lines) 99 | { 100 | line.Stroke = brush; 101 | } 102 | } 103 | } 104 | 105 | private void ViewClosed(object sender, EventArgs e) 106 | { 107 | if (_codingConventionsCancellationTokenSource != default(CancellationTokenSource)) 108 | { 109 | _codingConventionsCancellationTokenSource.Cancel(); 110 | } 111 | 112 | _view.LayoutChanged -= OnViewLayoutChanged; 113 | _view.Closed -= ViewClosed; 114 | if (_settingsChanged != null) 115 | { 116 | _settingsChanged.PropertyChanged -= SettingsChanged; 117 | _settingsChanged = null; 118 | } 119 | 120 | if (_guidelineBrush != null) 121 | { 122 | _guidelineBrush.BrushChanged -= GuidelineBrushChanged; 123 | _guidelineBrush = null; 124 | } 125 | } 126 | 127 | private void InitializeGuidelines(IEnumerable guideLinePositions) 128 | { 129 | var initialGuidelines = GuidelinesFromSettings(guideLinePositions); 130 | CreateVisualLines(initialGuidelines); 131 | } 132 | 133 | private void SettingsChanged(object sender, PropertyChangedEventArgs e) 134 | { 135 | if (!_isUsingCodingConvention && sender is ITextEditorGuidesSettings settings && e.PropertyName == nameof(ITextEditorGuidesSettings.GuideLinePositionsInChars)) 136 | { 137 | var guidelines = GuidelinesFromSettings(settings.GuideLinePositionsInChars); 138 | GuidelinesChanged(guidelines); 139 | } 140 | } 141 | 142 | /// 143 | /// Given a collection of column numbers, create a collection. 144 | /// The guidelines will all have the default (null) brush. 145 | /// 146 | /// The position of each guideline in characters from the left edge. 147 | /// A collection. 148 | private static IEnumerable GuidelinesFromSettings(IEnumerable guideLinePositions) 149 | { 150 | var guidelines = from column in guideLinePositions select new Guideline(column, null); 151 | return guidelines; 152 | } 153 | 154 | private void GuidelinesChanged(IEnumerable newGuidelines) 155 | { 156 | if (HaveGuidelinesChanged(newGuidelines)) 157 | { 158 | CreateVisualLines(newGuidelines); 159 | UpdatePositions(); 160 | AddGuidelinesToAdornmentLayer(); 161 | } 162 | } 163 | 164 | private void OnViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) 165 | { 166 | var fUpdatePositions = false; 167 | 168 | var lineSource = _view.FormattedLineSource; 169 | if (lineSource == null) 170 | { 171 | return; 172 | } 173 | 174 | if (_columnWidth != lineSource.ColumnWidth) 175 | { 176 | _columnWidth = lineSource.ColumnWidth; 177 | fUpdatePositions = true; 178 | } 179 | 180 | if (_baseIndentation != lineSource.BaseIndentation) 181 | { 182 | _baseIndentation = lineSource.BaseIndentation; 183 | fUpdatePositions = true; 184 | } 185 | 186 | if (fUpdatePositions || 187 | e.VerticalTranslation || 188 | e.NewViewState.ViewportTop != e.OldViewState.ViewportTop || 189 | e.NewViewState.ViewportBottom != e.OldViewState.ViewportBottom) 190 | { 191 | UpdatePositions(); 192 | } 193 | 194 | if (!_firstLayoutDone) 195 | { 196 | AddGuidelinesToAdornmentLayer(); 197 | _firstLayoutDone = true; 198 | } 199 | } 200 | 201 | /// 202 | /// Create the vertical lines. 203 | /// 204 | /// The collection of guidelines with position and style information. 205 | private void CreateVisualLines(IEnumerable guidelines) 206 | { 207 | _lines = (from guideline in guidelines select CreateLine(guideline)).ToArray(); 208 | } 209 | 210 | /// 211 | /// Create a single vertical column guide with the current stroke parameters for 212 | /// the given column in the current viewport. 213 | /// 214 | /// The columnar position of the new guideline. 215 | /// The new vertical column guide. 216 | private Line CreateLine(Guideline guideline) 217 | { 218 | var strokeParameters = guideline.StrokeParameters ?? _strokeParameters; 219 | var line = new Line 220 | { 221 | DataContext = guideline, 222 | Stroke = strokeParameters.Brush, 223 | StrokeThickness = strokeParameters.StrokeThickness, 224 | StrokeDashArray = strokeParameters.StrokeDashArray 225 | }; 226 | 227 | return line; 228 | } 229 | 230 | /// 231 | /// Update the rendering position of the given line to the current viewport. 232 | /// 233 | /// The line to update. 234 | private void UpdatePosition(Line line) 235 | { 236 | var guideline = (Guideline)line.DataContext; 237 | 238 | line.X1 = line.X2 = _baseIndentation + 0.5 + (guideline.Column * _columnWidth); 239 | line.Y1 = _view.ViewportTop; 240 | line.Y2 = _view.ViewportBottom; 241 | } 242 | 243 | /// 244 | /// Update all line positions when the viewport changes. 245 | /// 246 | private void UpdatePositions() 247 | { 248 | foreach (var line in _lines) 249 | { 250 | UpdatePosition(line); 251 | } 252 | } 253 | 254 | private void AddGuidelinesToAdornmentLayer() 255 | { 256 | // Get a reference to our adornment layer. 257 | var adornmentLayer = _view.GetAdornmentLayer(ColumnGuideAdornmentFactory.AdornmentLayerName); 258 | if (adornmentLayer == null) 259 | { 260 | return; 261 | } 262 | 263 | adornmentLayer.RemoveAllAdornments(); 264 | 265 | // Add the guidelines to the adornment layer and make them relative to the viewport. 266 | foreach (UIElement element in _lines) 267 | { 268 | adornmentLayer.AddAdornment(AdornmentPositioningBehavior.OwnerControlled, null, null, element, null); 269 | } 270 | } 271 | 272 | /// 273 | /// Try to load guideline positions from .editorconfig 274 | /// 275 | /// The coding conventions (.editorconfig) manager. 276 | /// Editor's WPF view. 277 | /// A task which completes when the convention has been loaded and applied. 278 | private async Task LoadGuidelinesFromEditorConfigAsync(CodingConventions codingConventions, IWpfTextView view) 279 | { 280 | CancellationToken cancellationToken = _codingConventionsCancellationTokenSource.Token; 281 | CodingConventions.Context context = await codingConventions.CreateContextAsync(view, cancellationToken).ConfigureAwait(false); 282 | if (context is null) 283 | { 284 | return; 285 | } 286 | 287 | context.ConventionsChanged += UpdateGuidelinesFromCodingConvention; 288 | cancellationToken.Register(() => context.ConventionsChanged -= UpdateGuidelinesFromCodingConvention); 289 | 290 | UpdateGuidelinesFromCodingConvention(context); 291 | } 292 | 293 | private void UpdateGuidelinesFromCodingConvention(CodingConventions.Context context) 294 | { 295 | if (_codingConventionsCancellationTokenSource.Token.IsCancellationRequested) 296 | { 297 | return; 298 | } 299 | 300 | StrokeParameters strokeParameters = null; 301 | 302 | if (context.TryGetCurrentSetting("guidelines_style", out string guidelines_style)) 303 | { 304 | if (TryParseStrokeParametersFromCodingConvention(guidelines_style, out strokeParameters)) 305 | { 306 | _isUsingCodingConvention = true; 307 | strokeParameters.Freeze(); 308 | } 309 | } 310 | 311 | ICollection guidelines = null; 312 | 313 | if (context.TryGetCurrentSetting("guidelines", out string guidelinesConventionValue)) 314 | { 315 | guidelines = ParseGuidelinesFromCodingConvention(guidelinesConventionValue, strokeParameters); 316 | } 317 | 318 | // Also support max_line_length: https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#max_line_length 319 | if (context.TryGetCurrentSetting("max_line_length", out string max_line_length) && TryParsePosition(max_line_length, out int maxLineLengthValue)) 320 | { 321 | (guidelines ?? (guidelines = new List())).Add(new Guideline(maxLineLengthValue, strokeParameters)); 322 | } 323 | 324 | if (guidelines != null) 325 | { 326 | // Override 'classic' settings. 327 | _isUsingCodingConvention = true; 328 | 329 | // TODO: await JoinableTaskFactory.SwitchToMainThreadAsync(); 330 | #pragma warning disable VSTHRD001 // Avoid legacy thread switching APIs 331 | _ = _view.VisualElement.Dispatcher.BeginInvoke(new Action>(GuidelinesChanged), guidelines); 332 | #pragma warning restore VSTHRD001 // Avoid legacy thread switching APIs 333 | } 334 | 335 | if (_isUsingCodingConvention && !s_sentEditorConfigTelemetry) 336 | { 337 | var eventTelemetry = new EventTelemetry("EditorConfig"); 338 | if (!string.IsNullOrEmpty(guidelinesConventionValue)) 339 | { 340 | eventTelemetry.Properties.Add("Convention", guidelinesConventionValue); 341 | } 342 | 343 | if (!string.IsNullOrEmpty(max_line_length)) 344 | { 345 | eventTelemetry.Properties.Add(nameof(max_line_length), max_line_length); 346 | } 347 | 348 | if (!string.IsNullOrEmpty(guidelines_style)) 349 | { 350 | eventTelemetry.Properties.Add(nameof(guidelines_style), guidelines_style); 351 | } 352 | 353 | ColumnGuideAdornmentFactory.AddGuidelinesToTelemetry(eventTelemetry, guidelines); 354 | Telemetry.Client.TrackEvent(eventTelemetry); 355 | s_sentEditorConfigTelemetry = true; 356 | } 357 | } 358 | 359 | private bool HaveGuidelinesChanged(IEnumerable newGuidelines) 360 | { 361 | if (_lines == null) 362 | { 363 | return true; 364 | } 365 | 366 | var currentGuidelines = from line in _lines select (Guideline)line.DataContext; 367 | return !currentGuidelines.SequenceEqual(newGuidelines); 368 | } 369 | 370 | public void Dispose() => _codingConventionsCancellationTokenSource.Dispose(); 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/ColumnGuide/ColumnGuideFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Microsoft.ApplicationInsights.DataContracts; 4 | using Microsoft.VisualStudio.Text.Editor; 5 | using Microsoft.VisualStudio.Utilities; 6 | using System.Collections.Generic; 7 | using System.ComponentModel; 8 | using System.ComponentModel.Composition; 9 | using System.Windows.Media; 10 | using static System.Globalization.CultureInfo; 11 | 12 | namespace EditorGuidelines 13 | { 14 | #region Adornment Factory 15 | /// 16 | /// Establishes an to place the adornment on and exports the 17 | /// that instantiates the adornment on the event of a 's creation 18 | /// 19 | [Export(typeof(IWpfTextViewCreationListener))] 20 | [ContentType("text")] 21 | [TextViewRole(PredefinedTextViewRoles.Document)] 22 | internal sealed class ColumnGuideAdornmentFactory : IWpfTextViewCreationListener, IPartImportsSatisfiedNotification 23 | { 24 | public const string AdornmentLayerName = "ColumnGuide"; 25 | 26 | /// 27 | /// Defines the adornment layer for the adornment. This layer is ordered 28 | /// below the text in the Z-order 29 | /// 30 | [Export(typeof(AdornmentLayerDefinition))] 31 | [Name(AdornmentLayerName)] 32 | [Order(Before = PredefinedAdornmentLayers.Text)] 33 | [TextViewRole(PredefinedTextViewRoles.Document)] 34 | public AdornmentLayerDefinition editorAdornmentLayer = null; 35 | 36 | /// 37 | /// Instantiates a ColumnGuide manager when a textView is created. 38 | /// 39 | /// The upon which the adornment should be placed 40 | public void TextViewCreated(IWpfTextView textView) 41 | { 42 | // Always create the adornment, even if there are no guidelines, since we 43 | // respond to dynamic changes. 44 | var _ = new ColumnGuideAdornment(textView, TextEditorGuidesSettings, GuidelineBrush, CodingConventions); 45 | } 46 | 47 | public void OnImportsSatisfied() 48 | { 49 | TrackSettings(global::EditorGuidelines.Telemetry.CreateInitializeTelemetryItem(nameof(ColumnGuideAdornmentFactory) + " initialized")); 50 | 51 | GuidelineBrush.BrushChanged += (sender, newBrush) => 52 | { 53 | Telemetry.Client.TrackEvent("GuidelineColorChanged", new Dictionary { ["Color"] = newBrush.ToString(InvariantCulture) }); 54 | }; 55 | 56 | if (TextEditorGuidesSettings is INotifyPropertyChanged settingsChanged) 57 | { 58 | settingsChanged.PropertyChanged += OnSettingsChanged; 59 | } 60 | } 61 | 62 | private void OnSettingsChanged(object sender, PropertyChangedEventArgs e) 63 | { 64 | if (e.PropertyName == nameof(ITextEditorGuidesSettings.GuideLinePositionsInChars)) 65 | { 66 | TrackSettings("SettingsChanged"); 67 | } 68 | } 69 | 70 | private void TrackSettings(string eventName) => TrackSettings(new EventTelemetry(eventName)); 71 | 72 | private void TrackSettings(EventTelemetry telemetry) 73 | { 74 | AddBrushColorAndGuidelinePositionsToTelemetry(telemetry, GuidelineBrush.Brush, TextEditorGuidesSettings.GuideLinePositionsInChars); 75 | Telemetry.Client.TrackEvent(telemetry); 76 | } 77 | 78 | internal static void AddBrushColorAndGuidelinePositionsToTelemetry(EventTelemetry eventTelemetry, Brush brush, IEnumerable positions) 79 | { 80 | var telemetryProperties = eventTelemetry.Properties; 81 | 82 | if (brush != null) 83 | { 84 | telemetryProperties.Add("Color", brush.ToString(InvariantCulture) ?? "unknown"); 85 | 86 | if (brush.Opacity != 1.0) 87 | { 88 | eventTelemetry.Metrics.Add("Opacity", brush.Opacity); 89 | } 90 | } 91 | 92 | var count = 0; 93 | foreach (var column in positions) 94 | { 95 | telemetryProperties.Add("guide" + count.ToString(InvariantCulture), column.ToString(InvariantCulture)); 96 | count++; 97 | } 98 | 99 | eventTelemetry.Metrics.Add("Count", count); 100 | } 101 | 102 | internal static void AddGuidelinesToTelemetry(EventTelemetry eventTelemetry, IEnumerable guidelines) 103 | { 104 | var telemetryProperties = eventTelemetry.Properties; 105 | 106 | var count = 0; 107 | foreach (var guideline in guidelines) 108 | { 109 | telemetryProperties.Add("guide" + count.ToString(InvariantCulture), guideline.ToString()); 110 | count++; 111 | } 112 | 113 | eventTelemetry.Metrics.Add("Count", count); 114 | } 115 | 116 | [Import] 117 | private ITextEditorGuidesSettings TextEditorGuidesSettings { get; set; } 118 | 119 | [Import] 120 | private GuidelineBrush GuidelineBrush { get; set; } 121 | 122 | [Import] 123 | private CodingConventions CodingConventions { get; set; } 124 | 125 | [Import] 126 | private HostServices HostServices { get; set; } 127 | } 128 | #endregion //Adornment Factory 129 | } 130 | -------------------------------------------------------------------------------- /src/ColumnGuide/EditorGuidelinesPackage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.Design; 6 | using System.Runtime.InteropServices; 7 | using System.Threading; 8 | using Microsoft.VisualStudio; 9 | using Microsoft.VisualStudio.Shell; 10 | using Microsoft.VisualStudio.Shell.Interop; 11 | using Microsoft.VisualStudio.Text.Editor; 12 | using Microsoft.VisualStudio.TextManager.Interop; 13 | using static System.Globalization.CultureInfo; 14 | 15 | namespace EditorGuidelines 16 | { 17 | /// 18 | /// This is the class that implements the package exposed by this assembly. 19 | /// 20 | /// The minimum requirement for a class to be considered a valid package for Visual Studio 21 | /// is to implement the IVsPackage interface and register itself with the shell. 22 | /// This package uses the helper classes defined inside the Managed Package Framework (MPF) 23 | /// to do it: it derives from the Package class that provides the implementation of the 24 | /// IVsPackage interface and uses the registration attributes defined in the framework to 25 | /// register itself and its components with the shell. 26 | /// 27 | // This attribute tells the PkgDef creation utility (CreatePkgDef.exe) that this class is 28 | // a package. 29 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 30 | // This attribute is needed to let the shell know that this package exposes some menus. 31 | [ProvideMenuResource("Menus.ctmenu", version: 2)] 32 | [Guid(PackageGuidString)] 33 | public sealed class EditorGuidelinesPackage : AsyncPackage 34 | { 35 | // Must match the guidEditorGuidelinesPackage value in the .vsct 36 | public const string PackageGuidString = "a0b80b01-be16-4c42-ab44-7f8d057faa2f"; 37 | 38 | // Must match the guidEditorGuidelinesPackageCmdSet value in the .vsct 39 | public static readonly Guid CommandSet = new Guid("5aa4cf31-6030-4655-99e7-239b331103f3"); 40 | 41 | private static class CommandIds 42 | { 43 | // Must match the cmdid values in the .vsct 44 | public const int AddColumnGuideline = 0x100; 45 | public const int RemoveColumnGuideline = 0x101; 46 | public const int RemoveAllColumnGuidelines = 0x103; 47 | } 48 | 49 | /// 50 | /// Default constructor of the package. 51 | /// Inside this method you can place any initialization code that does not require 52 | /// any Visual Studio service because at this point the package object is created but 53 | /// not sited yet inside Visual Studio environment. The place to do all the other 54 | /// initialization is the Initialize method. 55 | /// 56 | public EditorGuidelinesPackage() 57 | { 58 | _addGuidelineCommand = new OleMenuCommand(AddColumnGuideExecuted, null, AddColumnGuideBeforeQueryStatus, new CommandID(CommandSet, CommandIds.AddColumnGuideline)) 59 | { 60 | ParametersDescription = "" 61 | }; 62 | 63 | _removeGuidelineCommand = new OleMenuCommand(RemoveColumnGuideExecuted, null, RemoveColumnGuideBeforeChangeQueryStatus, new CommandID(CommandSet, CommandIds.RemoveColumnGuideline)) 64 | { 65 | ParametersDescription = "" 66 | }; 67 | } 68 | 69 | ///////////////////////////////////////////////////////////////////////////// 70 | // Overridden Package Implementation 71 | #region Package Members 72 | 73 | /// 74 | /// Initialization of the package; this method is called right after the package is sited, so this is the place 75 | /// where you can put all the initialization code that rely on services provided by VisualStudio. 76 | /// 77 | protected override async System.Threading.Tasks.Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 78 | { 79 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); 80 | Telemetry.Client.TrackEvent(nameof(EditorGuidelinesPackage) + "." + nameof(Initialize), new Dictionary() { ["VSVersion"] = GetShellVersion() }); 81 | 82 | // Add our command handlers for menu (commands must exist in the .vsct file) 83 | #pragma warning disable VSTHRD103 // Call async methods when in an async method. We're already on the main thread. 84 | if (GetService(typeof(IMenuCommandService)) is OleMenuCommandService mcs) 85 | #pragma warning restore VSTHRD103 // Call async methods when in an async method 86 | { 87 | mcs.AddCommand(_addGuidelineCommand); 88 | mcs.AddCommand(_removeGuidelineCommand); 89 | mcs.AddCommand(new MenuCommand(RemoveAllGuidelinesExecuted, new CommandID(CommandSet, CommandIds.RemoveAllColumnGuidelines))); 90 | } 91 | } 92 | 93 | #endregion 94 | 95 | private string GetShellVersion() 96 | { 97 | ThreadHelper.ThrowIfNotOnUIThread(); 98 | if (GetService(typeof(SVsShell)) is IVsShell shell) 99 | { 100 | if (ErrorHandler.Succeeded(shell.GetProperty((int)__VSSPROPID5.VSSPROPID_ReleaseVersion, out var obj)) && obj != null) 101 | { 102 | return obj.ToString(); 103 | 104 | } 105 | } 106 | 107 | return "Unknown"; 108 | } 109 | 110 | private readonly OleMenuCommand _addGuidelineCommand; 111 | private readonly OleMenuCommand _removeGuidelineCommand; 112 | 113 | private void AddColumnGuideBeforeQueryStatus(object sender, EventArgs e) 114 | { 115 | ThreadHelper.ThrowIfNotOnUIThread(); 116 | var currentColumn = GetCurrentEditorColumn(); 117 | _addGuidelineCommand.Enabled = TextEditorGuidesSettingsRendezvous.Instance.CanAddGuideline(currentColumn); 118 | } 119 | 120 | private void RemoveColumnGuideBeforeChangeQueryStatus(object sender, EventArgs e) 121 | { 122 | ThreadHelper.ThrowIfNotOnUIThread(); 123 | var currentColumn = GetCurrentEditorColumn(); 124 | _removeGuidelineCommand.Enabled = TextEditorGuidesSettingsRendezvous.Instance.CanRemoveGuideline(currentColumn); 125 | } 126 | 127 | /// 128 | /// Determine the applicable column number for an add or remove command. 129 | /// The column is parsed from command arguments, if present. Otherwise 130 | /// the current position of the caret is used to determine the column. 131 | /// 132 | /// Event args passed to the command handler. 133 | /// The column number. May be negative to indicate the column number is unavailable. 134 | /// The column number parsed from event args was not a valid integer. 135 | private int GetApplicableColumn(EventArgs e) 136 | { 137 | var inValue = ((OleMenuCmdEventArgs)e).InValue as string; 138 | if (!string.IsNullOrEmpty(inValue)) 139 | { 140 | if (!int.TryParse(inValue, out var column) || column < 0) 141 | { 142 | throw new ArgumentException(Resources.InvalidColumn); 143 | } 144 | 145 | Telemetry.Client.TrackEvent("Command parameter used"); 146 | return column; 147 | } 148 | 149 | ThreadHelper.ThrowIfNotOnUIThread(); 150 | return GetCurrentEditorColumn(); 151 | } 152 | 153 | private void AddColumnGuideExecuted(object sender, EventArgs e) 154 | { 155 | ThreadHelper.ThrowIfNotOnUIThread(); 156 | var column = GetApplicableColumn(e); 157 | if (column >= 0) 158 | { 159 | Telemetry.Client.TrackEvent(nameof(AddColumnGuideExecuted), new Dictionary() { ["Column"] = column.ToString(InvariantCulture) }); 160 | TextEditorGuidesSettingsRendezvous.Instance.AddGuideline(column); 161 | } 162 | } 163 | 164 | private void RemoveColumnGuideExecuted(object sender, EventArgs e) 165 | { 166 | ThreadHelper.ThrowIfNotOnUIThread(); 167 | var column = GetApplicableColumn(e); 168 | if (column >= 0) 169 | { 170 | Telemetry.Client.TrackEvent(nameof(RemoveColumnGuideExecuted), new Dictionary() { ["Column"] = column.ToString(InvariantCulture) }); 171 | TextEditorGuidesSettingsRendezvous.Instance.RemoveGuideline(column); 172 | } 173 | } 174 | 175 | private void RemoveAllGuidelinesExecuted(object sender, EventArgs e) 176 | { 177 | Telemetry.Client.TrackEvent(nameof(RemoveAllGuidelinesExecuted)); 178 | TextEditorGuidesSettingsRendezvous.Instance.RemoveAllGuidelines(); 179 | } 180 | 181 | /// 182 | /// Find the active text view (if any) in the active document. 183 | /// 184 | /// The IVsTextView of the active view, or null if there is no active document or the 185 | /// active view in the active document is not a text view. 186 | private IVsTextView GetActiveTextView() 187 | { 188 | ThreadHelper.ThrowIfNotOnUIThread(); 189 | if (!(GetService(typeof(IVsMonitorSelection)) is IVsMonitorSelection selection)) 190 | { 191 | throw new InvalidOperationException(Resources.MissingIVsMonitorSelectionService); 192 | } 193 | 194 | ErrorHandler.ThrowOnFailure(selection.GetCurrentElementValue((uint)VSConstants.VSSELELEMID.SEID_DocumentFrame, out var frameObj)); 195 | 196 | return frameObj is IVsWindowFrame frame ? GetActiveView(frame) : null; 197 | } 198 | 199 | private static IVsTextView GetActiveView(IVsWindowFrame windowFrame) 200 | { 201 | if (windowFrame == null) 202 | { 203 | throw new ArgumentNullException(nameof(windowFrame)); 204 | } 205 | 206 | ThreadHelper.ThrowIfNotOnUIThread(); 207 | ErrorHandler.ThrowOnFailure(windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out var pvar)); 208 | 209 | var textView = pvar as IVsTextView; 210 | if (textView == null) 211 | { 212 | if (pvar is IVsCodeWindow codeWin) 213 | { 214 | ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView)); 215 | } 216 | } 217 | 218 | return textView; 219 | } 220 | 221 | private static IWpfTextView GetTextViewFromVsTextView(IVsTextView view) 222 | { 223 | if (view == null) 224 | { 225 | throw new ArgumentNullException(nameof(view)); 226 | } 227 | 228 | if (!(view is IVsUserData userData)) 229 | { 230 | throw new InvalidOperationException(); 231 | } 232 | 233 | if (VSConstants.S_OK != userData.GetData(Microsoft.VisualStudio.Editor.DefGuidList.guidIWpfTextViewHost, out var objTextViewHost)) 234 | { 235 | throw new InvalidOperationException(); 236 | } 237 | 238 | if (!(objTextViewHost is IWpfTextViewHost textViewHost)) 239 | { 240 | throw new InvalidOperationException(); 241 | } 242 | 243 | return textViewHost.TextView; 244 | } 245 | 246 | /// 247 | /// Given an IWpfTextView, find the position of the caret and report its column number 248 | /// The column number is 0-based 249 | /// 250 | /// The text view containing the caret 251 | /// The column number of the caret's position. When the caret is at the leftmost column, the return value is zero. 252 | private static int GetCaretColumn(IWpfTextView textView) 253 | { 254 | // This is the code the editor uses to populate the status bar. Thanks, Jack! 255 | var caretViewLine = textView.Caret.ContainingTextViewLine; 256 | var columnWidth = textView.FormattedLineSource.ColumnWidth; 257 | return (int)Math.Round((textView.Caret.Left - caretViewLine.Left) / columnWidth); 258 | } 259 | 260 | private int GetCurrentEditorColumn() 261 | { 262 | ThreadHelper.ThrowIfNotOnUIThread(); 263 | var view = GetActiveTextView(); 264 | if (view == null) 265 | { 266 | return -1; 267 | } 268 | 269 | try 270 | { 271 | var textView = GetTextViewFromVsTextView(view); 272 | var column = GetCaretColumn(textView); 273 | 274 | // Note: GetCaretColumn returns 0-based positions. Guidelines are 1-based positions. 275 | // However, do not subtract one here since the caret is positioned to the left of 276 | // the given column and the guidelines are positioned to the right. We want the 277 | // guideline to line up with the current caret position. e.g. When the caret is 278 | // at position 1 (zero-based), the status bar says column 2. We want to add a 279 | // guideline for column 1 since that will place the guideline where the caret is. 280 | return column; 281 | } 282 | catch (InvalidOperationException) 283 | { 284 | return -1; 285 | } 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/ColumnGuide/EditorGuidelinesPackage.vsct: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 34 | 35 | 36 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | &Guidelines 65 | 66 | 67 | 68 | 69 | 70 | 72 | 73 | 80 | 81 | 90 | 91 | 100 | 101 | 108 | 109 | 110 | 111 | 112 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /src/ColumnGuide/Guideline.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using static System.Globalization.CultureInfo; 5 | using static System.FormattableString; 6 | 7 | namespace EditorGuidelines 8 | { 9 | /// 10 | /// Represents the position and style of a single guideline. 11 | /// 12 | internal sealed class Guideline : IEquatable 13 | { 14 | /// 15 | /// The column number of the guideline. The text editor convention is to number the leftmost 16 | /// column as 1. The guideline is drawn to the right of the given column. That way, when you 17 | /// put a guideline at column 80, for example, you make room for up to 80 characters to the 18 | /// left of the guideline. We also allow placing a guideline at column zero, meaning it's to 19 | /// the left of the first column of text. 20 | /// 21 | public int Column { get; } 22 | 23 | /// 24 | /// The stroke parameters of the guideline. See . 25 | /// 26 | public StrokeParameters StrokeParameters { get; } 27 | 28 | /// 29 | /// Construct a new . 30 | /// 31 | /// The column number. Must be between 0 and 10,000. 32 | /// The stroke parameters for this guideline. If null, then 33 | /// the default brush from Fonts & Colors is used with default . 34 | public Guideline(int column, StrokeParameters strokeParameters) 35 | { 36 | if (!IsValidColumn(column)) 37 | { 38 | throw new ArgumentOutOfRangeException(nameof(column), Resources.AddGuidelineParameterOutOfRange); 39 | } 40 | 41 | Column = column; 42 | StrokeParameters = strokeParameters; 43 | } 44 | 45 | /// 46 | /// Test if this guideline is equivalent to another. 47 | /// 48 | /// The other guideline. 49 | /// True if the two guidelines may be considered equal. 50 | public bool Equals(Guideline other) => 51 | Column == other.Column && 52 | (StrokeParameters is null ? other.StrokeParameters is null : StrokeParameters.Equals(other.StrokeParameters)); 53 | 54 | public override bool Equals(object obj) => obj is Guideline other && Equals(other); 55 | 56 | public override int GetHashCode() => unchecked(Column.GetHashCode() + (StrokeParameters?.GetHashCode() ?? 0)); 57 | 58 | public override string ToString() => StrokeParameters is null ? Column.ToString(InvariantCulture) : Invariant($"{Column} {StrokeParameters}"); 59 | 60 | /// 61 | /// Check if the given column is valid. 62 | /// Negative values are not allowed. 63 | /// Zero is allowed (per user request) 64 | /// 10000 seems like a sensible upper limit. 65 | /// 66 | /// The column. 67 | /// True if is valid. 68 | internal static bool IsValidColumn(int column) => 69 | 0 <= column && column <= 10000; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ColumnGuide/GuidelineBrush.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Microsoft.VisualStudio.Text.Classification; 4 | using System; 5 | using System.ComponentModel.Composition; 6 | using System.Windows.Media; 7 | 8 | namespace EditorGuidelines 9 | { 10 | [Export] 11 | internal class GuidelineBrush 12 | { 13 | private Brush _brush; 14 | private readonly IEditorFormatMap _formatMap; 15 | 16 | [ImportingConstructor] 17 | #pragma warning disable IDE0051 // Remove unused private members 18 | private GuidelineBrush(IEditorFormatMapService editorFormatMapService) 19 | #pragma warning restore IDE0051 // Remove unused private members 20 | { 21 | _formatMap = editorFormatMapService.GetEditorFormatMap("text"); 22 | _formatMap.FormatMappingChanged += OnFormatMappingChanged; 23 | } 24 | 25 | private void OnFormatMappingChanged(object sender, FormatItemsEventArgs e) 26 | { 27 | if (e.ChangedItems.Contains(GuidelineColorDefinition.c_name)) 28 | { 29 | _brush = GetGuidelineBrushFromFontsAndColors(); 30 | BrushChanged?.Invoke(this, _brush); 31 | } 32 | } 33 | 34 | public Brush Brush => _brush ?? (_brush = GetGuidelineBrushFromFontsAndColors()); 35 | 36 | public event EventHandler BrushChanged; 37 | 38 | private Brush GetGuidelineBrushFromFontsAndColors() 39 | { 40 | var resourceDictionary = _formatMap.GetProperties(GuidelineColorDefinition.c_name); 41 | return resourceDictionary.Contains(EditorFormatDefinition.BackgroundBrushId) 42 | ? resourceDictionary[EditorFormatDefinition.BackgroundBrushId] as Brush 43 | : null; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ColumnGuide/GuidelineColorDefinition.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Microsoft.VisualStudio.Text.Classification; 4 | using Microsoft.VisualStudio.Utilities; 5 | using System.ComponentModel.Composition; 6 | using System.Windows.Media; 7 | 8 | namespace EditorGuidelines 9 | { 10 | [Export(typeof(EditorFormatDefinition)), UserVisible(true), Name(c_name)] 11 | public class GuidelineColorDefinition : EditorFormatDefinition 12 | { 13 | internal const string c_name = "Guideline"; 14 | 15 | public GuidelineColorDefinition() 16 | { 17 | DisplayName = c_name; 18 | ForegroundCustomizable = false; 19 | BackgroundColor = Colors.DarkRed; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ColumnGuide/HostServices.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.ComponentModel.Composition; 5 | using Microsoft.VisualStudio.Shell; 6 | using Microsoft.VisualStudio.Shell.Interop; 7 | 8 | namespace EditorGuidelines 9 | { 10 | [Export] 11 | internal sealed class HostServices 12 | { 13 | [Import(typeof(SVsServiceProvider))] 14 | private IServiceProvider ServiceProvider 15 | { 16 | get; 17 | set; 18 | } 19 | 20 | public T GetService(Type serviceType) where T : class 21 | => ServiceProvider.GetService(serviceType) as T; 22 | 23 | // Add services here 24 | 25 | public IVsSettingsManager SettingsManagerService 26 | => GetService(typeof(SVsSettingsManager)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ColumnGuide/ITextBufferExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Microsoft.VisualStudio.Text; 4 | using Microsoft.VisualStudio.TextManager.Interop; 5 | 6 | namespace EditorGuidelines 7 | { 8 | internal static class ITextBufferExtensions 9 | { 10 | public static bool TryGetTextDocument(this ITextBuffer textBuffer, out ITextDocument textDocument) 11 | { 12 | if (textBuffer == null) 13 | { 14 | textDocument = null; 15 | return false; 16 | } 17 | 18 | return textBuffer.Properties.TryGetProperty(typeof(ITextDocument), out textDocument); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ColumnGuide/ITextEditorGuidesSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System.Collections.Generic; 4 | 5 | namespace EditorGuidelines 6 | { 7 | internal interface ITextEditorGuidesSettings 8 | { 9 | IEnumerable GuideLinePositionsInChars { get; } 10 | 11 | bool DontShowVsVersionWarning { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ColumnGuide/ITextEditorGuidesSettingsChanger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | 5 | namespace EditorGuidelines 6 | { 7 | public interface ITextEditorGuidesSettingsChanger 8 | { 9 | bool AddGuideline(int column); 10 | bool RemoveGuideline(int column); 11 | bool CanAddGuideline(int column); 12 | bool CanRemoveGuideline(int column); 13 | void RemoveAllGuidelines(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ColumnGuide/IWpfTextViewExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Microsoft.VisualStudio.Text; 4 | using Microsoft.VisualStudio.Text.Editor; 5 | 6 | namespace EditorGuidelines 7 | { 8 | internal static class IWpfTextViewExtensions 9 | { 10 | public static bool TryGetTextDocument(this IWpfTextView view, out ITextDocument textDocument) 11 | { 12 | if (view == null) 13 | { 14 | textDocument = null; 15 | return false; 16 | } 17 | 18 | // Try the TextBuffer first. If that fails, try the DocumentBuffer on the TextDataModel. 19 | return view.TextBuffer.TryGetTextDocument(out textDocument) 20 | || view.TextDataModel.DocumentBuffer.TryGetTextDocument(out textDocument); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ColumnGuide/LineStyle.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | namespace EditorGuidelines 4 | { 5 | /// 6 | /// Line style for the guideline. 7 | /// 8 | internal enum LineStyle 9 | { 10 | /// 11 | /// A 1:3 dotted line 12 | /// 13 | Dotted, 14 | 15 | /// 16 | /// A 3:1 dashed line 17 | /// 18 | Dashed, 19 | 20 | /// 21 | /// A solid line. 22 | /// 23 | Solid 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ColumnGuide/Parser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Windows.Media; 7 | 8 | namespace EditorGuidelines 9 | { 10 | /// 11 | /// Parser utilities. 12 | /// 13 | internal static class Parser 14 | { 15 | /// 16 | /// Note: Semicolon is not a valid separator because it's treated as a comment in .editorconfig 17 | /// 18 | private static readonly char[] s_separators = { ',', ':', ' ' }; 19 | 20 | private static readonly char[] s_space= { ' ' }; 21 | 22 | private static readonly char[] s_comma = { ',' }; 23 | 24 | public static HashSet ParseGuidelinesFromCodingConvention(string codingConvention, StrokeParameters fallbackStrokeParameters) 25 | { 26 | // First try parsing as a sequence of columns and styles separated by commas. 27 | var result = ParseGuidelines(codingConvention, fallbackStrokeParameters); 28 | if (result != null) 29 | { 30 | return result; 31 | } 32 | 33 | // Fall back to parsing as just a set of column positions, ignoring any unparsable values. 34 | result = new HashSet(); 35 | foreach (var position in GetTokens(codingConvention, s_separators)) 36 | { 37 | if (TryParsePosition(position, out int column)) 38 | { 39 | result.Add(new Guideline(column, fallbackStrokeParameters)); 40 | } 41 | } 42 | 43 | return result; 44 | } 45 | 46 | /// 47 | /// Try to parse as a sequence of columns and styles separated by commas. 48 | /// e.g. 40 1px solid red, 80 2px dashed blue 49 | /// The style part is optional but, if present, it must be well-formed. 50 | /// 51 | /// The coding convention. 52 | /// Stroke parameters to use when the style is not specified. 53 | /// The set of guidelines if successful, or null if not. 54 | private static HashSet ParseGuidelines(string codingConvention, StrokeParameters fallbackStrokeParameters) 55 | { 56 | var set = new HashSet(); 57 | foreach (var token in GetTokens(codingConvention, s_comma)) 58 | { 59 | var partEnumerator = GetTokens(token, s_space); 60 | if (!partEnumerator.MoveNext()) 61 | { 62 | // Empty token. Ignore and continue. 63 | continue; 64 | } 65 | 66 | if (!TryParsePosition(partEnumerator.Current, out var column)) 67 | { 68 | return null; 69 | } 70 | 71 | var strokeParameters = fallbackStrokeParameters; 72 | if (partEnumerator.MoveNext() && !TryParseStrokeParameters(partEnumerator, out strokeParameters)) 73 | { 74 | return null; 75 | } 76 | 77 | set.Add(new Guideline(column, strokeParameters?.Freeze())); 78 | } 79 | 80 | return set; 81 | } 82 | 83 | public static bool TryParsePosition(string text, out int column) 84 | => int.TryParse(text, out column) && Guideline.IsValidColumn(column); 85 | 86 | /// 87 | /// The guideline_style looks like this: 88 | /// guidelines_style = 1px dotted 80FF0000 89 | /// Meaning single pixel, dotted style, color red, 50% opaque 90 | /// 91 | /// 1px specifies the width in pixels. 92 | /// dotted specifies the line style.Simple to support: solid, dotted and dashed 93 | /// 94 | /// The value read from guidelines_style editorconfig. 95 | /// The parsed stroke parameters. 96 | /// True if parameters were parsed. False otherwise. 97 | public static bool TryParseStrokeParametersFromCodingConvention(string text, out StrokeParameters strokeParameters) 98 | { 99 | var tokensEnumerator = GetTokens(text, s_separators).GetEnumerator(); 100 | if (!tokensEnumerator.MoveNext()) 101 | { 102 | strokeParameters = null; 103 | return false; 104 | } 105 | 106 | return TryParseStrokeParameters(tokensEnumerator, out strokeParameters); 107 | } 108 | 109 | private static bool TryParseStrokeParameters(TokenEnumerator tokensEnumerator, out StrokeParameters strokeParameters) 110 | { 111 | strokeParameters = null; 112 | 113 | // Pixel width (stroke thickness) 114 | var token = tokensEnumerator.Current; 115 | if (!token.EndsWith("px", StringComparison.Ordinal)) 116 | { 117 | return false; 118 | } 119 | 120 | if (!double.TryParse(token.Substring(0, token.Length - 2), out var strokeThickness)) 121 | { 122 | return false; 123 | } 124 | 125 | if (strokeThickness < 0 || strokeThickness > 50) 126 | { 127 | return false; 128 | } 129 | 130 | strokeParameters = new StrokeParameters 131 | { 132 | Brush = new SolidColorBrush(Colors.Black), 133 | StrokeThickness = strokeThickness 134 | }; 135 | 136 | if (!tokensEnumerator.MoveNext()) 137 | { 138 | return true; 139 | } 140 | 141 | // Line style 142 | token = tokensEnumerator.Current; 143 | if (Enum.TryParse(token, ignoreCase: true, out var lineStyle)) 144 | { 145 | strokeParameters.LineStyle = lineStyle; 146 | } 147 | 148 | if (!tokensEnumerator.MoveNext()) 149 | { 150 | return true; 151 | } 152 | 153 | // Color 154 | token = tokensEnumerator.Current; 155 | if (TryParseColor(token, out var color)) 156 | { 157 | strokeParameters.Brush = new SolidColorBrush(color); 158 | } 159 | 160 | // Ignore trailing tokens. 161 | return true; 162 | } 163 | 164 | private struct TokenEnumerator 165 | { 166 | private readonly string _text; 167 | private readonly char[] _separators; 168 | private int _iStart; 169 | 170 | public TokenEnumerator(string text, char[] separators) 171 | { 172 | _text = text; 173 | _separators = separators; 174 | _iStart = 0; 175 | Current = null; 176 | } 177 | 178 | public TokenEnumerator GetEnumerator() => this; 179 | 180 | public bool MoveNext() 181 | { 182 | if (_text == null) 183 | { 184 | return false; 185 | } 186 | 187 | // Equivalent of text.Split(separators, StringSplitOptions.RemoveEmptyEntries); 188 | while (_iStart < _text.Length) 189 | { 190 | var iNextSeparator = _text.IndexOfAny(_separators, _iStart); 191 | if (iNextSeparator < 0) 192 | { 193 | iNextSeparator = _text.Length; 194 | } 195 | 196 | var tokenLength = iNextSeparator - _iStart; 197 | if (tokenLength > 0) 198 | { 199 | Current = _text.Substring(_iStart, tokenLength); 200 | _iStart = iNextSeparator + 1; 201 | return true; 202 | } 203 | 204 | _iStart = iNextSeparator + 1; 205 | } 206 | 207 | return false; 208 | } 209 | 210 | public string Current { get; private set; } 211 | } 212 | 213 | private static TokenEnumerator GetTokens(string text, char[] separators) => new TokenEnumerator(text, separators); 214 | 215 | private static bool IsInRange(char ch, char low, char high) 216 | => (uint)(ch - low) <= high - low; 217 | 218 | private static bool IsHexDigit(char ch) => 219 | IsInRange(ch, '0', '9') || 220 | IsInRange(ch, 'A', 'F') || 221 | IsInRange(ch, 'a', 'f'); 222 | 223 | private static bool IsRGBorARGBValue(string text) 224 | => (text.Length == 6 || text.Length == 8) && text.All(IsHexDigit); 225 | 226 | private static bool TryParseColor(string text, out Color color) 227 | { 228 | if (IsRGBorARGBValue(text)) 229 | { 230 | // There are no 6 or 8 letter named colors spelled only with the letters A to F. 231 | text = "#" + text; 232 | } 233 | 234 | try 235 | { 236 | var colorObj = ColorConverter.ConvertFromString(text) as Color?; 237 | if (colorObj.HasValue) 238 | { 239 | color = colorObj.Value; 240 | return true; 241 | } 242 | } 243 | catch (FormatException) 244 | { 245 | } 246 | 247 | color = default; 248 | return false; 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/ColumnGuide/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Microsoft.VisualStudio.Shell; 4 | using System; 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | using System.Runtime.InteropServices; 8 | 9 | // General Information about an assembly is controlled through the following 10 | // set of attributes. Change these attribute values to modify the information 11 | // associated with an assembly. 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | [assembly: ComVisible(false)] 15 | [assembly: CLSCompliant(false)] 16 | 17 | [assembly: ProvideCodeBase(CodeBase = "Microsoft.ApplicationInsights.dll")] 18 | #if !Dev17 19 | // Include CodingConventions prior to VS 2022 20 | [assembly: ProvideCodeBase(CodeBase = "Microsoft.VisualStudio.CodingConventions.dll")] 21 | #endif 22 | 23 | [assembly: InternalsVisibleTo("ColumnGuideTests")] 24 | -------------------------------------------------------------------------------- /src/ColumnGuide/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace EditorGuidelines { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EditorGuidelines.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to The parameter must be between 0 and 10,000. 65 | /// 66 | internal static string AddGuidelineParameterOutOfRange { 67 | get { 68 | return ResourceManager.GetString("AddGuidelineParameterOutOfRange", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Invalid column.. 74 | /// 75 | internal static string InvalidColumn { 76 | get { 77 | return ResourceManager.GetString("InvalidColumn", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Missing IVsMonitorSelection service.. 83 | /// 84 | internal static string MissingIVsMonitorSelectionService { 85 | get { 86 | return ResourceManager.GetString("MissingIVsMonitorSelectionService", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to The parameter must be between 0 and 10,000. 92 | /// 93 | internal static string RemoveGuidelineParameterOutOfRange { 94 | get { 95 | return ResourceManager.GetString("RemoveGuidelineParameterOutOfRange", resourceCulture); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ColumnGuide/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The parameter must be between 0 and 10,000 122 | Error message when an invalid value is passed to AddGuideline 123 | 124 | 125 | Invalid column. 126 | Error message when a bad column number is passed to add or remove column. 127 | 128 | 129 | Missing IVsMonitorSelection service. 130 | Error message when the IVsMonitorSelection service cannot be obtained. 131 | 132 | 133 | The parameter must be between 0 and 10,000 134 | Error message when an invalid value is passed to RemoveGuideline 135 | 136 | -------------------------------------------------------------------------------- /src/ColumnGuide/Resources/Images_32bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharring/EditorGuidelines/26d146201a3d56b3f741cf860b5eea44cb42e726/src/ColumnGuide/Resources/Images_32bit.png -------------------------------------------------------------------------------- /src/ColumnGuide/Resources/Package.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pharring/EditorGuidelines/26d146201a3d56b3f741cf860b5eea44cb42e726/src/ColumnGuide/Resources/Package.ico -------------------------------------------------------------------------------- /src/ColumnGuide/StrokeParameters.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.Windows.Media; 5 | using static System.FormattableString; 6 | 7 | namespace EditorGuidelines 8 | { 9 | /// 10 | /// Represents the collection of parameters that define the drawing style of a vertical guideline. 11 | /// 12 | internal sealed class StrokeParameters : IEquatable 13 | { 14 | /// 15 | /// The default thickness in pixels. 16 | /// 17 | private const double c_defaultThickness = 1.0; 18 | 19 | private static readonly DoubleCollection s_dottedDashArray = new DoubleCollection(new[] { 1.0, 3.0 }); 20 | private static readonly DoubleCollection s_dashedDashArray = new DoubleCollection(new[] { 3.0, 1.0 }); 21 | private static readonly DoubleCollection s_solidDashArray = new DoubleCollection(); 22 | 23 | /// 24 | /// Create an instance from the given brush. The default thickness and line style are used. 25 | /// 26 | /// The brush. 27 | /// A new instance. 28 | /// This is used to create a when all we have is a 29 | /// brush from Fonts & Colors. 30 | public static StrokeParameters FromBrush(Brush brush) => new StrokeParameters { Brush = brush }; 31 | 32 | /// 33 | /// The brush. For all practical purposes this is a SolidColorBrush. 34 | /// 35 | public Brush Brush { get; set; } 36 | 37 | /// 38 | /// The stroke thickness in pixels. 39 | /// 40 | public double StrokeThickness { get; set; } = c_defaultThickness; 41 | 42 | /// 43 | /// The line style. 44 | /// 45 | public LineStyle LineStyle { get; set; } = LineStyle.Dotted; 46 | 47 | /// 48 | /// The stroke dash array used to define the drawing style to WPF. 49 | /// 50 | public DoubleCollection StrokeDashArray 51 | { 52 | get 53 | { 54 | switch (LineStyle) 55 | { 56 | case LineStyle.Dotted: 57 | return s_dottedDashArray; 58 | 59 | case LineStyle.Dashed: 60 | return s_dashedDashArray; 61 | 62 | case LineStyle.Solid: 63 | return s_solidDashArray; 64 | 65 | default: 66 | throw new IndexOutOfRangeException(); 67 | } 68 | } 69 | } 70 | 71 | /// 72 | /// Freeze the parameters (especially the Brush). Necessary before passing it across threads. 73 | /// 74 | /// The frozen object. 75 | public StrokeParameters Freeze() 76 | { 77 | Brush.Freeze(); 78 | return this; 79 | } 80 | 81 | /// 82 | /// Extract the brush's color. 83 | /// 84 | /// Internal for testing. 85 | internal Color BrushColor => (Brush is SolidColorBrush solidColorBrush) ? solidColorBrush.Color : default; 86 | 87 | public bool Equals(StrokeParameters other) => other != null && BrushColor == other.BrushColor && StrokeThickness == other.StrokeThickness && LineStyle == other.LineStyle; 88 | 89 | public override bool Equals(object obj) => obj is StrokeParameters other && Equals(other); 90 | 91 | public override int GetHashCode() => unchecked(Brush.GetHashCode() + StrokeThickness.GetHashCode() + LineStyle.GetHashCode()); 92 | 93 | public override string ToString() => Invariant($"{StrokeThickness}px {LineStyle} {BrushColor}"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/ColumnGuide/Telemetry.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Microsoft.ApplicationInsights; 4 | using Microsoft.ApplicationInsights.Channel; 5 | using Microsoft.ApplicationInsights.DataContracts; 6 | using Microsoft.ApplicationInsights.Extensibility; 7 | using System; 8 | using System.Security.Cryptography; 9 | 10 | namespace EditorGuidelines 11 | { 12 | internal static class Telemetry 13 | { 14 | private const string c_instrumentationKey = "f8324fcc-eb39-4931-bebc-968aab7d3d7d"; 15 | 16 | public static TelemetryClient Client { get; } = CreateClient(); 17 | 18 | /// 19 | /// Create a telemetry item for the 'initialize' event with additional properties 20 | /// that only need to be sent once. 21 | /// 22 | /// The name of the initialize event. 23 | /// A custom event telemetry with additional context for OS and component version. 24 | public static EventTelemetry CreateInitializeTelemetryItem(string name) 25 | { 26 | var eventTelemetry = new EventTelemetry(name); 27 | eventTelemetry.Context.Device.OperatingSystem = Environment.OSVersion.ToString(); 28 | eventTelemetry.Context.Component.Version = typeof(Telemetry).Assembly.GetName().Version.ToString(); 29 | return eventTelemetry; 30 | } 31 | 32 | private static TelemetryClient CreateClient() 33 | { 34 | var configuration = new TelemetryConfiguration 35 | { 36 | InstrumentationKey = c_instrumentationKey, 37 | TelemetryChannel = new InMemoryChannel 38 | { 39 | #if DEBUG 40 | DeveloperMode = true 41 | #else 42 | DeveloperMode = false 43 | #endif 44 | } 45 | }; 46 | 47 | // Keep this context as small as possible since it's sent with every event. 48 | var client = new TelemetryClient(configuration); 49 | client.Context.User.Id = Anonymize(Environment.UserDomainName + "\\" + Environment.UserName); 50 | client.Context.Session.Id = Convert.ToBase64String(GetRandomBytes(length:6)); 51 | return client; 52 | } 53 | 54 | private static byte[] GetRandomBytes(int length) 55 | { 56 | var buff = new byte[length]; 57 | using (var rnd = RandomNumberGenerator.Create()) 58 | { 59 | rnd.GetBytes(buff); 60 | } 61 | 62 | return buff; 63 | } 64 | 65 | private static string Anonymize(string str) 66 | { 67 | #pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms 68 | using (var sha1 = SHA1.Create()) 69 | #pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms 70 | { 71 | var inputBytes = System.Text.Encoding.Unicode.GetBytes(str); 72 | var hash = sha1.ComputeHash(inputBytes); 73 | var base64 = Convert.ToBase64String(hash, 0, 6); 74 | return base64; 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ColumnGuide/TextEditorGuidesSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.Composition; 6 | using System.Runtime.InteropServices; 7 | using System.Windows.Media; 8 | using Microsoft.VisualStudio.Shell.Interop; 9 | using System.ComponentModel; 10 | using System.Text; 11 | 12 | using static System.Globalization.CultureInfo; 13 | using static EditorGuidelines.Guideline; 14 | 15 | #pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread. SettingsStore is thread safe. 16 | namespace EditorGuidelines 17 | { 18 | [Export(typeof(ITextEditorGuidesSettings))] 19 | [Export(typeof(ITextEditorGuidesSettingsChanger))] 20 | internal sealed class TextEditorGuidesSettings : ITextEditorGuidesSettings, INotifyPropertyChanged, ITextEditorGuidesSettingsChanger 21 | { 22 | private const int c_maxGuides = 12; 23 | 24 | [Import] 25 | private Lazy HostServices { get; set; } 26 | 27 | private IVsSettingsStore ReadOnlyUserSettings 28 | { 29 | get 30 | { 31 | var manager = HostServices.Value.SettingsManagerService; 32 | Marshal.ThrowExceptionForHR(manager.GetReadOnlySettingsStore((uint)__VsSettingsScope.SettingsScope_UserSettings, out var store)); 33 | return store; 34 | } 35 | } 36 | 37 | private IVsWritableSettingsStore ReadWriteUserSettings 38 | { 39 | get 40 | { 41 | var manager = HostServices.Value.SettingsManagerService; 42 | Marshal.ThrowExceptionForHR(manager.GetWritableSettingsStore((uint)__VsSettingsScope.SettingsScope_UserSettings, out var store)); 43 | return store; 44 | } 45 | } 46 | 47 | 48 | private string GetUserSettingsString(string key, string value) 49 | { 50 | var store = ReadOnlyUserSettings; 51 | Marshal.ThrowExceptionForHR(store.GetStringOrDefault(key, value, string.Empty, out var result)); 52 | return result; 53 | } 54 | 55 | private void WriteUserSettingsString(string key, string propertyName, string value) 56 | { 57 | var store = ReadWriteUserSettings; 58 | Marshal.ThrowExceptionForHR(store.SetString(key, propertyName, value)); 59 | } 60 | 61 | private void WriteSettings(Color color, IEnumerable columns) 62 | { 63 | var value = ComposeSettingsString(color, columns); 64 | GuidelinesConfiguration = value; 65 | } 66 | 67 | private static string ComposeSettingsString(Color color, IEnumerable columns) 68 | { 69 | var sb = new StringBuilder(); 70 | sb.AppendFormat(InvariantCulture, "RGB({0},{1},{2})", color.R, color.G, color.B); 71 | var columnsEnumerator = columns.GetEnumerator(); 72 | if( columnsEnumerator.MoveNext() ) 73 | { 74 | sb.AppendFormat(InvariantCulture, " {0}", columnsEnumerator.Current); 75 | while( columnsEnumerator.MoveNext() ) 76 | { 77 | sb.AppendFormat(InvariantCulture, ", {0}", columnsEnumerator.Current); 78 | } 79 | } 80 | 81 | return sb.ToString(); 82 | } 83 | 84 | #region ITextEditorGuidesSettingsChanger Members 85 | 86 | public bool AddGuideline(int column) 87 | { 88 | if (!IsValidColumn(column)) 89 | { 90 | throw new ArgumentOutOfRangeException(nameof(column), Resources.AddGuidelineParameterOutOfRange); 91 | } 92 | 93 | if (GetCountOfGuidelines() >= c_maxGuides) 94 | { 95 | return false; // Cannot add more than _maxGuides guidelines 96 | } 97 | 98 | // Check for duplicates 99 | var columns = new List(GuideLinePositionsInChars); 100 | if (columns.Contains(column)) 101 | { 102 | return false; 103 | } 104 | 105 | columns.Add(column); 106 | 107 | WriteSettings(GuidelinesColor, columns); 108 | return true; 109 | } 110 | 111 | public bool RemoveGuideline(int column) 112 | { 113 | if (!IsValidColumn(column)) 114 | { 115 | throw new ArgumentOutOfRangeException(nameof(column), Resources.RemoveGuidelineParameterOutOfRange); 116 | } 117 | 118 | var columns = new List(GuideLinePositionsInChars); 119 | if (!columns.Remove(column)) 120 | { 121 | // Not present 122 | // Allow user to remove the last column even if they're not on the right column 123 | if (columns.Count != 1) 124 | { 125 | return false; 126 | } 127 | 128 | columns.Clear(); 129 | } 130 | 131 | WriteSettings(GuidelinesColor, columns); 132 | return true; 133 | } 134 | 135 | public bool CanAddGuideline(int column) 136 | => IsValidColumn(column) 137 | && GetCountOfGuidelines() < c_maxGuides 138 | && !IsGuidelinePresent(column); 139 | 140 | public bool CanRemoveGuideline(int column) 141 | => IsValidColumn(column) 142 | && (IsGuidelinePresent(column) || HasExactlyOneGuideline()); // Allow user to remove the last guideline regardless of the column 143 | 144 | public void RemoveAllGuidelines() 145 | => WriteSettings(GuidelinesColor, Array.Empty()); 146 | 147 | #endregion 148 | 149 | private bool HasExactlyOneGuideline() 150 | { 151 | using (var enumerator = GuideLinePositionsInChars.GetEnumerator()) 152 | { 153 | return enumerator.MoveNext() && !enumerator.MoveNext(); 154 | } 155 | } 156 | 157 | private int GetCountOfGuidelines() 158 | { 159 | var i = 0; 160 | foreach (var value in GuideLinePositionsInChars) 161 | { 162 | i++; 163 | } 164 | 165 | return i; 166 | } 167 | 168 | private bool IsGuidelinePresent(int column) 169 | { 170 | foreach (var value in GuideLinePositionsInChars) 171 | { 172 | if (value == column) 173 | { 174 | return true; 175 | } 176 | } 177 | 178 | return false; 179 | } 180 | 181 | private string _guidelinesConfiguration; 182 | private string GuidelinesConfiguration 183 | { 184 | get 185 | { 186 | if (_guidelinesConfiguration == null) 187 | { 188 | _guidelinesConfiguration = GetUserSettingsString(c_textEditor, "Guides").Trim(); 189 | } 190 | 191 | return _guidelinesConfiguration; 192 | } 193 | 194 | set 195 | { 196 | if (value != _guidelinesConfiguration) 197 | { 198 | _guidelinesConfiguration = value; 199 | WriteUserSettingsString(c_textEditor, "Guides", value); 200 | FirePropertyChanged(nameof(ITextEditorGuidesSettings.GuideLinePositionsInChars)); 201 | } 202 | } 203 | } 204 | 205 | private void FirePropertyChanged(string propertyName) 206 | => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 207 | 208 | // Parse a color out of a string that begins like "RGB(255,0,0)" 209 | public Color GuidelinesColor 210 | { 211 | get 212 | { 213 | var config = GuidelinesConfiguration; 214 | if (!string.IsNullOrEmpty(config) && config.StartsWith("RGB(", StringComparison.Ordinal)) 215 | { 216 | var lastParen = config.IndexOf(')'); 217 | if (lastParen > 4) 218 | { 219 | var rgbs = config.Substring(4, lastParen - 4).Split(','); 220 | 221 | if (rgbs.Length >= 3) 222 | { 223 | if (byte.TryParse(rgbs[0], out var r) && 224 | byte.TryParse(rgbs[1], out var g) && 225 | byte.TryParse(rgbs[2], out var b)) 226 | { 227 | return Color.FromRgb(r, g, b); 228 | } 229 | } 230 | } 231 | } 232 | 233 | return Colors.DarkRed; 234 | } 235 | 236 | set => WriteSettings(value, GuideLinePositionsInChars); 237 | } 238 | 239 | // Parse a list of integer values out of a string that looks like "RGB(255,0,0) 1,5,10,80" 240 | public IEnumerable GuideLinePositionsInChars 241 | { 242 | get 243 | { 244 | var config = GuidelinesConfiguration; 245 | if (string.IsNullOrEmpty(config)) 246 | { 247 | yield break; 248 | } 249 | 250 | if (!config.StartsWith("RGB(", StringComparison.Ordinal)) 251 | { 252 | yield break; 253 | } 254 | 255 | var lastParen = config.IndexOf(')'); 256 | if (lastParen <= 4) 257 | { 258 | yield break; 259 | } 260 | 261 | var columns = config.Substring(lastParen + 1).Split(','); 262 | 263 | var columnCount = 0; 264 | foreach (var columnText in columns) 265 | { 266 | var column = -1; 267 | if (int.TryParse(columnText, out column) && column >= 0 /*Note: VS 2008 didn't allow zero, but we do, per user request*/ ) 268 | { 269 | columnCount++; 270 | yield return column; 271 | if (columnCount >= c_maxGuides) 272 | { 273 | break; 274 | } 275 | } 276 | } 277 | } 278 | } 279 | 280 | public bool DontShowVsVersionWarning 281 | { 282 | get 283 | { 284 | var store = ReadOnlyUserSettings; 285 | Marshal.ThrowExceptionForHR(store.GetBoolOrDefault(c_textEditor, c_dontShowVsVersionWarningPropertyName, 0, out int value)); 286 | return value != 0; 287 | } 288 | 289 | set 290 | { 291 | var store = ReadWriteUserSettings; 292 | Marshal.ThrowExceptionForHR(store.SetBool(c_textEditor, c_dontShowVsVersionWarningPropertyName, value ? 1 : 0)); 293 | } 294 | } 295 | 296 | private const string c_textEditor = "Text Editor"; 297 | private const string c_dontShowVsVersionWarningPropertyName = "DontShowEditorGuidelinesVsVersionWarning"; 298 | 299 | #region INotifyPropertyChanged Members 300 | 301 | public event PropertyChangedEventHandler PropertyChanged; 302 | 303 | #endregion 304 | } 305 | } 306 | #pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread 307 | -------------------------------------------------------------------------------- /src/ColumnGuide/TextEditorGuidesSettingsRendezvous.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Microsoft.VisualStudio.ComponentModelHost; 4 | using Microsoft.VisualStudio.Shell; 5 | 6 | namespace EditorGuidelines 7 | { 8 | internal static class TextEditorGuidesSettingsRendezvous 9 | { 10 | private static ITextEditorGuidesSettingsChanger s_instance; 11 | public static ITextEditorGuidesSettingsChanger Instance => s_instance ?? (s_instance = GetGlobalInstance()); 12 | 13 | private static ITextEditorGuidesSettingsChanger GetGlobalInstance() 14 | { 15 | var componentModel = (IComponentModel)Package.GetGlobalService(typeof(SComponentModel)); 16 | return componentModel.GetService(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ColumnGuide/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Editor Guidelines.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31410.414 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AFD82285-D26D-4361-AEC9-74A1D04AD45D}" 7 | ProjectSection(SolutionItems) = preProject 8 | ..\.editorconfig = ..\.editorconfig 9 | ..\CHANGELOG.md = ..\CHANGELOG.md 10 | ..\Directory.Build.props = ..\Directory.Build.props 11 | ..\Directory.Build.targets = ..\Directory.Build.targets 12 | ..\global.json = ..\global.json 13 | ..\NuGet.Config = ..\NuGet.Config 14 | EndProjectSection 15 | ProjectSection(FolderGlobals) = preProject 16 | D_5_4EditorGuidelines_4global_1json__JsonSchema = 17 | EndProjectSection 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "marketplace", "marketplace", "{205F9C77-7D58-42CD-A882-5D73C40E6184}" 20 | ProjectSection(SolutionItems) = preProject 21 | ..\marketplace\overview.Dev17.md = ..\marketplace\overview.Dev17.md 22 | ..\marketplace\overview.md = ..\marketplace\overview.md 23 | ..\marketplace\publishManifest.Dev17.json = ..\marketplace\publishManifest.Dev17.json 24 | ..\marketplace\publishManifest.json = ..\marketplace\publishManifest.json 25 | EndProjectSection 26 | EndProject 27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{A5C7FD7F-D246-4BBA-9A49-BBDEC89ACEA6}" 28 | ProjectSection(SolutionItems) = preProject 29 | ..\marketplace\images\CommandWindow.png = ..\marketplace\images\CommandWindow.png 30 | ..\marketplace\images\ContextMenu.png = ..\marketplace\images\ContextMenu.png 31 | ..\marketplace\images\FontsAndColors.png = ..\marketplace\images\FontsAndColors.png 32 | EndProjectSection 33 | EndProject 34 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ColumnGuideTests", "..\test\ColumnGuideTests\ColumnGuideTests.csproj", "{38832BD0-0B36-410A-914D-2E5BA06EBA6A}" 35 | EndProject 36 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "ColumnGuide", "ColumnGuide\ColumnGuide.shproj", "{61D4B2D7-4433-4C71-BE24-57A36758ED99}" 37 | EndProject 38 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VSIX", "VSIX\VSIX.csproj", "{5ADE1B3F-BC61-42F3-A467-F782D4EFF07E}" 39 | EndProject 40 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VSIX_Dev17", "VSIX_Dev17\VSIX_Dev17.csproj", "{FEB7F80E-E565-4119-A18E-D2B9F5E95383}" 41 | EndProject 42 | Global 43 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 44 | Debug|Any CPU = Debug|Any CPU 45 | Release|Any CPU = Release|Any CPU 46 | EndGlobalSection 47 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 48 | {38832BD0-0B36-410A-914D-2E5BA06EBA6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {38832BD0-0B36-410A-914D-2E5BA06EBA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {38832BD0-0B36-410A-914D-2E5BA06EBA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {38832BD0-0B36-410A-914D-2E5BA06EBA6A}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {5ADE1B3F-BC61-42F3-A467-F782D4EFF07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {5ADE1B3F-BC61-42F3-A467-F782D4EFF07E}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {5ADE1B3F-BC61-42F3-A467-F782D4EFF07E}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {5ADE1B3F-BC61-42F3-A467-F782D4EFF07E}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {FEB7F80E-E565-4119-A18E-D2B9F5E95383}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {FEB7F80E-E565-4119-A18E-D2B9F5E95383}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {FEB7F80E-E565-4119-A18E-D2B9F5E95383}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {FEB7F80E-E565-4119-A18E-D2B9F5E95383}.Release|Any CPU.Build.0 = Release|Any CPU 60 | EndGlobalSection 61 | GlobalSection(SolutionProperties) = preSolution 62 | HideSolutionNode = FALSE 63 | EndGlobalSection 64 | GlobalSection(NestedProjects) = preSolution 65 | {A5C7FD7F-D246-4BBA-9A49-BBDEC89ACEA6} = {205F9C77-7D58-42CD-A882-5D73C40E6184} 66 | EndGlobalSection 67 | GlobalSection(ExtensibilityGlobals) = postSolution 68 | SolutionGuid = {64E283A2-5918-46A4-9D2B-83B601BBEBBD} 69 | EndGlobalSection 70 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 71 | ColumnGuide\ColumnGuide.projitems*{5ade1b3f-bc61-42f3-a467-f782d4eff07e}*SharedItemsImports = 5 72 | ColumnGuide\ColumnGuide.projitems*{61d4b2d7-4433-4c71-be24-57a36758ed99}*SharedItemsImports = 13 73 | ColumnGuide\ColumnGuide.projitems*{feb7f80e-e565-4119-a18e-d2b9f5e95383}*SharedItemsImports = 5 74 | EndGlobalSection 75 | EndGlobal 76 | -------------------------------------------------------------------------------- /src/VSIX/CodingConventions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System.ComponentModel.Composition; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.VisualStudio.CodingConventions; 7 | using Microsoft.VisualStudio.Text.Editor; 8 | 9 | namespace EditorGuidelines 10 | { 11 | /// 12 | /// Coding conventions support via Microsoft.VisualStudio.CodingConventions assembly. 13 | /// 14 | [Export] 15 | internal sealed class CodingConventions 16 | { 17 | /// 18 | /// Try to import from Microsoft.VisualStudio.CodingConventions. 19 | /// 20 | [Import(AllowDefault = true)] 21 | private ICodingConventionsManager CodingConventionsManager { get; set; } 22 | 23 | public async Task CreateContextAsync(IWpfTextView view, CancellationToken cancellationToken) 24 | { 25 | if (CodingConventionsManager is null) 26 | { 27 | // Coding Conventions not available in this SKU. 28 | return null; 29 | } 30 | 31 | if (!view.TryGetTextDocument(out var textDocument)) 32 | { 33 | return null; 34 | } 35 | 36 | string filePath = textDocument.FilePath; 37 | ICodingConventionContext codingConventionContext = await CodingConventionsManager.GetConventionContextAsync(filePath, cancellationToken).ConfigureAwait(false); 38 | return new Context(codingConventionContext, cancellationToken); 39 | } 40 | 41 | /// 42 | /// Coding conventions narrowed to a single file. 43 | /// 44 | public class Context 45 | { 46 | private readonly ICodingConventionContext _innerContext; 47 | 48 | public Context(ICodingConventionContext innerContext, CancellationToken cancellationToken) 49 | { 50 | _innerContext = innerContext; 51 | _innerContext.CodingConventionsChangedAsync += OnCodingConventionsChangedAsync; 52 | cancellationToken.Register(() => _innerContext.CodingConventionsChangedAsync -= OnCodingConventionsChangedAsync); 53 | } 54 | 55 | private Task OnCodingConventionsChangedAsync(object sender, CodingConventionsChangedEventArgs arg) 56 | { 57 | ConventionsChanged?.Invoke(this); 58 | return Task.CompletedTask; 59 | } 60 | 61 | public delegate void ConventionsChangedEventHandler(Context sender); 62 | 63 | public event ConventionsChangedEventHandler ConventionsChanged; 64 | 65 | public bool TryGetCurrentSetting(string key, out string value) 66 | { 67 | return _innerContext.CurrentConventions.TryGetConventionValue(key, out value); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/VSIX/VSIX.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | net472 8 | EditorGuidelines 9 | EditorGuidelines 10 | en-US 11 | 12 | true 13 | true 14 | true 15 | false 16 | false 17 | true 18 | true 19 | 20 | Editor Guidelines 21 | Adds commands for the Editor Guidelines extension 22 | 23 | false 24 | 25 | Program 26 | $(DevEnvDir)devenv.exe 27 | /rootsuffix Exp 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | EditorGuidelines_128px.png 53 | Always 54 | true 55 | 56 | 57 | LICENSE 58 | PreserveNewest 59 | true 60 | 61 | 62 | EditorGuidelinesPackage.vsct 63 | Menus.ctmenu 64 | 65 | 66 | Designer 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | <_ReferenceCopyLocalBinaries Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' != '.pdb'" /> 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/VSIX/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Editor Guidelines 7 | Adds vertical column guides to the Visual Studio text editor. 8 | https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines 9 | LICENSE 10 | https://github.com/pharring/EditorGuidelines#getting-started 11 | https://github.com/pharring/EditorGuidelines/blob/master/CHANGELOG.md 12 | EditorGuidelines_128px.png 13 | Editor;Editing;Guidelines;Column 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/VSIX_Dev17/CodingConventions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System.ComponentModel.Composition; 4 | using System.Threading.Tasks; 5 | using System.Threading; 6 | using Microsoft.VisualStudio.Text.Editor; 7 | using System.Collections.Generic; 8 | 9 | namespace EditorGuidelines 10 | { 11 | /// 12 | /// Dev 17 support for Coding Conventions. Uses the dictionary supplied via . 13 | /// 14 | [Export] 15 | internal sealed class CodingConventions 16 | { 17 | public Task CreateContextAsync(IWpfTextView view, CancellationToken cancellationToken) 18 | { 19 | return Task.FromResult(new Context(view.Options, cancellationToken)); 20 | } 21 | 22 | /// 23 | /// Coding conventions narrowed to a single file. 24 | /// 25 | public class Context 26 | { 27 | private readonly IEditorOptions _editorOptions; 28 | private readonly CancellationToken _cancellationToken; 29 | private IReadOnlyDictionary _currentConventions; 30 | 31 | // This is the same as DefaultOptions.RawCodingConventionsSnapshotOptionName from the 17.6 32 | // editor SDK. However, by not referencing that constant, we can avoid taking a dependency 33 | // on the 17.6 SDK and can continue to load on earlier versions (albeit without 34 | // CodingConventions support). 35 | private const string c_codingConventionsSnapshotOptionName = "CodingConventionsSnapshot"; 36 | 37 | public Context(IEditorOptions editorOptions, CancellationToken cancellationToken) 38 | { 39 | _editorOptions = editorOptions; 40 | _cancellationToken = cancellationToken; 41 | 42 | _editorOptions.OptionChanged += OnEditorOptionChanged; 43 | _cancellationToken.Register(() => _editorOptions.OptionChanged -= OnEditorOptionChanged); 44 | } 45 | 46 | private void OnEditorOptionChanged(object sender, EditorOptionChangedEventArgs e) 47 | { 48 | if (e.OptionId == c_codingConventionsSnapshotOptionName) 49 | { 50 | _currentConventions = _editorOptions.GetOptionValue>(c_codingConventionsSnapshotOptionName); 51 | ConventionsChanged?.Invoke(this); 52 | } 53 | } 54 | 55 | public delegate void ConventionsChangedEventHandler(Context sender); 56 | 57 | public event ConventionsChangedEventHandler ConventionsChanged; 58 | 59 | public bool TryGetCurrentSetting(string key, out string value) 60 | { 61 | if (_currentConventions is null || !_currentConventions.TryGetValue(key, out object obj) || obj is null) 62 | { 63 | value = null; 64 | return false; 65 | } 66 | 67 | value = obj.ToString(); 68 | return !string.IsNullOrEmpty(value); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/VSIX_Dev17/VSIX_Dev17.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | net472 8 | EditorGuidelines 9 | EditorGuidelines 10 | en-US 11 | Dev17 12 | 13 | true 14 | true 15 | true 16 | false 17 | false 18 | true 19 | true 20 | 21 | Editor Guidelines 22 | Adds commands for the Editor Guidelines extension 23 | 24 | 25 | false 26 | 27 | Program 28 | $(DevEnvDir)devenv.exe 29 | /rootsuffix Exp 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | compile; build; native; contentfiles; analyzers; buildtransitive 44 | 45 | 46 | all 47 | runtime; build; native; contentfiles; analyzers; buildtransitive 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | EditorGuidelines_128px.png 56 | Always 57 | true 58 | 59 | 60 | LICENSE 61 | PreserveNewest 62 | true 63 | 64 | 65 | EditorGuidelinesPackage.vsct 66 | Menus.ctmenu 67 | 68 | 69 | Designer 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | <_ReferenceCopyLocalBinaries Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' != '.pdb'" /> 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/VSIX_Dev17/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Editor Guidelines 7 | Adds vertical column guides to the Visual Studio text editor. 8 | https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines 9 | LICENSE 10 | https://github.com/pharring/EditorGuidelines#getting-started 11 | https://github.com/pharring/EditorGuidelines/blob/master/CHANGELOG.md 12 | EditorGuidelines_128px.png 13 | Editor;Editing;Guidelines;Column 14 | 15 | 16 | 17 | amd64 18 | 19 | 20 | arm64 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/ColumnGuideTests/ColumnGuideTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | net472 7 | false 8 | 7.3 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/ColumnGuideTests/ParserTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Xunit; 4 | using EditorGuidelines; 5 | using System.Windows.Media; 6 | using System.Linq; 7 | 8 | namespace ColumnGuideTests 9 | { 10 | public class UnitTest1 11 | { 12 | [Theory] 13 | [InlineData("")] 14 | [InlineData("0", 0)] 15 | [InlineData("0 1", 0, 1)] 16 | [InlineData("0,1", 0, 1)] 17 | [InlineData("0:1,2", 0, 1, 2)] 18 | [InlineData("0 1,2 3:", 0, 1, 2, 3)] 19 | [InlineData("132:80, 40,50,60 4 8", 4, 8, 40, 50, 60, 80, 132)] 20 | [InlineData("80,80,80", 80)] 21 | [InlineData("-1, 99999, 80", 80)] 22 | [InlineData("ABC, 3.14, 80", 80)] 23 | public void ParseGuidelinePositionsTest(string codingConvention, params int[] expected) 24 | { 25 | var actual = Parser.ParseGuidelinesFromCodingConvention(codingConvention, null); 26 | Assert.True(actual.SetEquals(from column in expected select new Guideline(column, null))); 27 | } 28 | 29 | [Theory] 30 | [InlineData(null, false)] 31 | [InlineData("", false)] 32 | [InlineData(",", false)] 33 | [InlineData("2", false)] // No 'px' suffix 34 | [InlineData("-2px", false)] // -ve 35 | [InlineData("51px", false)] // Too big 36 | [InlineData("abpx", false)] // Not a number 37 | [InlineData("5px, unknown-style", true, 5)] // Unrecognized style, but otherwise valid 38 | [InlineData("5px, unknown-style, green", true, 5, default(LineStyle), 0xFF, 0x00, 0x80, 0x00)] // Unrecognized style, but color valid 39 | [InlineData("1px", true, 1.0)] 40 | [InlineData("0.5px", true, 0.5)] 41 | [InlineData("3px dashed", true, 3, LineStyle.Dashed)] 42 | [InlineData("1.9px solid red", true, 1.9, LineStyle.Solid, 0xFF, 0xFF, 0x00, 0x00)] 43 | [InlineData("2.00px dashed A0553201", true, 2, LineStyle.Dashed, 0xA0, 0x55, 0x32, 0x01)] 44 | [InlineData("1px solid FEDCBA", true, 1, LineStyle.Solid, 0xFF, 0xFE, 0xDC, 0xBA)] 45 | [InlineData("1px,solid,Not-a-real-color", true, 1, LineStyle.Solid)] 46 | [InlineData("4px:dotted:blue:ignored", true, 4, LineStyle.Dotted, 0xFF, 0x00, 0x00, 0xFF)] 47 | internal void ParseStyleTest(string text, bool expected, double expectedThickness = default, LineStyle expectedLineStyle = default, byte expectedA = 0xFF, byte expectedR = 0, byte expectedG = 0, byte expectedB = 0) 48 | { 49 | var actual = Parser.TryParseStrokeParametersFromCodingConvention(text, out var strokeParameters); 50 | Assert.Equal(expected, actual); 51 | 52 | if (expected) 53 | { 54 | Assert.Equal(expectedThickness, strokeParameters.StrokeThickness); 55 | Assert.Equal(expectedLineStyle, strokeParameters.LineStyle); 56 | var expectedColor = Color.FromArgb(expectedA, expectedR, expectedG, expectedB); 57 | Assert.Equal(expectedColor, strokeParameters.BrushColor); 58 | } 59 | } 60 | 61 | [Fact] 62 | public void ParseGuidelinesTest() 63 | { 64 | var codingConvention = "40 1px solid red, , 60, 80 0.5px dashed 10203040, 132 4px ignored"; 65 | Assert.True(Parser.TryParseStrokeParametersFromCodingConvention("1px dotted gold", out var fallbackStrokeParameters)); 66 | var actual = Parser.ParseGuidelinesFromCodingConvention(codingConvention, fallbackStrokeParameters); 67 | 68 | Assert.Equal(4, actual.Count); 69 | 70 | // Normal case 71 | Assert.Contains(new Guideline(40, new StrokeParameters { StrokeThickness = 1, LineStyle = LineStyle.Solid, Brush = Brushes.Red }), actual); 72 | 73 | // Uses fallback stroke parameters 74 | Assert.Contains(new Guideline(60, fallbackStrokeParameters), actual); 75 | 76 | // Normal case with hex color 77 | Assert.Contains(new Guideline(80, new StrokeParameters { StrokeThickness = 0.5, LineStyle = LineStyle.Dashed, Brush = new SolidColorBrush(Color.FromArgb(0x10, 0x20, 0x30, 0x40)) }), actual); 78 | 79 | // Partial style definition 80 | Assert.Contains(new Guideline(132, new StrokeParameters { StrokeThickness = 4, Brush = Brushes.Black }), actual); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/AbstractIntegrationTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using System.Windows; 6 | using System.Windows.Threading; 7 | using EditorGuidelinesTests.Harness; 8 | using EditorGuidelinesTests.Threading; 9 | using Microsoft.VisualStudio.Shell.Interop; 10 | using Microsoft.VisualStudio.Threading; 11 | using Xunit; 12 | 13 | namespace EditorGuidelinesTests 14 | { 15 | public abstract class AbstractIntegrationTest : IAsyncLifetime, IDisposable 16 | { 17 | private JoinableTaskContext _joinableTaskContext; 18 | private JoinableTaskCollection _joinableTaskCollection; 19 | private JoinableTaskFactory _joinableTaskFactory; 20 | 21 | private TestServices _testServices; 22 | 23 | protected AbstractIntegrationTest() 24 | { 25 | Assert.True(Application.Current.Dispatcher.CheckAccess()); 26 | 27 | if (ServiceProvider.GetService(typeof(SVsTaskSchedulerService)) is IVsTaskSchedulerService2 taskSchedulerService) 28 | { 29 | JoinableTaskContext = (JoinableTaskContext)taskSchedulerService.GetAsyncTaskContext(); 30 | } 31 | else 32 | { 33 | JoinableTaskContext = new JoinableTaskContext(); 34 | } 35 | } 36 | 37 | protected static IServiceProvider ServiceProvider => Microsoft.VisualStudio.Shell.ServiceProvider.GlobalProvider; 38 | 39 | public JoinableTaskContext JoinableTaskContext 40 | { 41 | get 42 | { 43 | return _joinableTaskContext ?? throw new InvalidOperationException(); 44 | } 45 | 46 | private set 47 | { 48 | if (value == _joinableTaskContext) 49 | { 50 | return; 51 | } 52 | 53 | if (value is null) 54 | { 55 | _joinableTaskContext = null; 56 | _joinableTaskCollection = null; 57 | _joinableTaskFactory = null; 58 | } 59 | else 60 | { 61 | _joinableTaskContext = value; 62 | _joinableTaskCollection = value.CreateCollection(); 63 | _joinableTaskFactory = value.CreateFactory(_joinableTaskCollection).WithPriority(Application.Current.Dispatcher, DispatcherPriority.Background); 64 | } 65 | } 66 | } 67 | 68 | protected JoinableTaskFactory JoinableTaskFactory => _joinableTaskFactory ?? throw new InvalidOperationException(); 69 | 70 | protected TestServices TestServices => _testServices ?? throw new InvalidOperationException(); 71 | 72 | public virtual Task InitializeAsync() 73 | { 74 | _testServices = new TestServices(JoinableTaskFactory, ServiceProvider); 75 | return Task.CompletedTask; 76 | } 77 | 78 | public virtual async Task DisposeAsync() 79 | { 80 | await _joinableTaskCollection.JoinTillEmptyAsync(); 81 | _testServices = null; 82 | JoinableTaskContext = null; 83 | } 84 | 85 | public virtual void Dispose() 86 | { 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/EditorGuidelinesTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net472 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Harness/AbstractServices.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using Microsoft.VisualStudio.Threading; 5 | 6 | namespace EditorGuidelinesTests.Harness 7 | { 8 | internal abstract class AbstractServices 9 | { 10 | protected AbstractServices(TestServices testServices) 11 | { 12 | TestServices = testServices; 13 | } 14 | 15 | protected TestServices TestServices { get; } 16 | protected JoinableTaskFactory JoinableTaskFactory => TestServices.JoinableTaskFactory; 17 | protected IServiceProvider ServiceProvider => TestServices.ServiceProvider; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Harness/KeyboardInput.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using WindowsInput.Native; 4 | 5 | namespace EditorGuidelinesTests.Harness 6 | { 7 | public readonly struct KeyboardInput 8 | { 9 | internal readonly object _value; 10 | 11 | private KeyboardInput(string value) 12 | { 13 | _value = value; 14 | } 15 | 16 | private KeyboardInput(char value) 17 | { 18 | _value = value; 19 | } 20 | 21 | private KeyboardInput(VirtualKeyCode value) 22 | { 23 | _value = value; 24 | } 25 | 26 | private KeyboardInput((VirtualKeyCode virtualKeyCode, ShiftState shiftState) value) 27 | { 28 | _value = value; 29 | } 30 | 31 | public static implicit operator KeyboardInput(string value) => new KeyboardInput(value); 32 | public static implicit operator KeyboardInput(char value) => new KeyboardInput(value); 33 | public static implicit operator KeyboardInput(VirtualKeyCode value) => new KeyboardInput(value); 34 | public static implicit operator KeyboardInput((VirtualKeyCode virtualKeyCode, ShiftState shiftState) value) => new KeyboardInput(value); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Harness/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace EditorGuidelinesTests.Harness 7 | { 8 | internal static class NativeMethods 9 | { 10 | private const string c_kernel32 = "kernel32.dll"; 11 | private const string c_user32 = "user32.dll"; 12 | 13 | public const uint SWP_NOZORDER = 4; 14 | 15 | [DllImport(c_user32, SetLastError = true)] 16 | public static extern IntPtr GetLastActivePopup(IntPtr hWnd); 17 | 18 | [DllImport(c_user32, SetLastError = true)] 19 | public static extern void SwitchToThisWindow(IntPtr hWnd, [MarshalAs(UnmanagedType.Bool)] bool fUnknown); 20 | 21 | [DllImport(c_user32, SetLastError = true)] 22 | [return: MarshalAs(UnmanagedType.Bool)] 23 | public static extern bool IsWindowVisible(IntPtr hWnd); 24 | 25 | [DllImport(c_user32, SetLastError = false)] 26 | [return: MarshalAs(UnmanagedType.Bool)] 27 | public static extern bool SetForegroundWindow(IntPtr hWnd); 28 | 29 | [DllImport(c_kernel32, SetLastError = true)] 30 | [return: MarshalAs(UnmanagedType.Bool)] 31 | public static extern bool AllocConsole(); 32 | 33 | [DllImport(c_kernel32, SetLastError = true)] 34 | [return: MarshalAs(UnmanagedType.Bool)] 35 | public static extern bool FreeConsole(); 36 | 37 | [DllImport(c_kernel32, SetLastError = false)] 38 | public static extern IntPtr GetConsoleWindow(); 39 | 40 | [DllImport(c_user32, SetLastError = true)] 41 | [return: MarshalAs(UnmanagedType.Bool)] 42 | public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); 43 | 44 | [DllImport(c_user32, CharSet = CharSet.Unicode)] 45 | public static extern short VkKeyScan(char ch); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Harness/SendInputServices.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | using WindowsInput; 7 | using WindowsInput.Native; 8 | 9 | namespace EditorGuidelinesTests.Harness 10 | { 11 | internal sealed class SendInputServices : AbstractServices 12 | { 13 | public SendInputServices(TestServices testServices) 14 | : base(testServices) 15 | { 16 | } 17 | 18 | public async Task SendAsync(params KeyboardInput[] inputs) 19 | { 20 | await SendAsync(inputSimulator => 21 | { 22 | foreach (var input in inputs) 23 | { 24 | SendInput(inputSimulator, input); 25 | } 26 | }); 27 | } 28 | 29 | private void SendInput(InputSimulator inputSimulator, KeyboardInput input) 30 | { 31 | switch (input._value) 32 | { 33 | case string stringValue: 34 | var text = stringValue.Replace("\r\n", "\r").Replace('\n', '\r'); 35 | foreach (var c in text) 36 | { 37 | SendInput(inputSimulator, c); 38 | } 39 | 40 | break; 41 | 42 | case char charValue: 43 | SendCharacter(inputSimulator, charValue); 44 | break; 45 | 46 | case VirtualKeyCode virtualKeyCode: 47 | SendVirtualKey(inputSimulator, virtualKeyCode); 48 | break; 49 | 50 | case ValueTuple modifiedVirtualKey: 51 | SendVirtualKey(inputSimulator, modifiedVirtualKey.Item1, modifiedVirtualKey.Item2); 52 | break; 53 | 54 | case null: 55 | throw new ArgumentNullException(nameof(input)); 56 | 57 | default: 58 | throw new InvalidOperationException("Not reachable"); 59 | } 60 | } 61 | 62 | private void SendCharacter(InputSimulator inputSimulator, char ch) 63 | { 64 | var keyCode = NativeMethods.VkKeyScan(ch); 65 | if (keyCode == -1) 66 | { 67 | // This is a Unicode character, or otherwise cannot be mapped to a virtual key code 68 | SendUnicodeCharacter(inputSimulator, ch); 69 | return; 70 | } 71 | 72 | var virtualKey = (VirtualKeyCode)(keyCode & 0xFF); 73 | var shiftState = (ShiftState)(((ushort)keyCode >> 8) & 0xFF); 74 | SendVirtualKey(inputSimulator, virtualKey, shiftState); 75 | } 76 | 77 | private void SendUnicodeCharacter(InputSimulator inputSimulator, char ch) 78 | { 79 | inputSimulator.Keyboard.TextEntry(ch); 80 | } 81 | 82 | private void SendVirtualKey(InputSimulator inputSimulator, VirtualKeyCode virtualKey, ShiftState shiftState = ShiftState.None) 83 | { 84 | var modifiers = new List(); 85 | if (shiftState.HasFlag(ShiftState.Shift)) 86 | { 87 | modifiers.Add(VirtualKeyCode.SHIFT); 88 | } 89 | 90 | if (shiftState.HasFlag(ShiftState.Ctrl)) 91 | { 92 | modifiers.Add(VirtualKeyCode.CONTROL); 93 | } 94 | 95 | if (shiftState.HasFlag(ShiftState.Alt)) 96 | { 97 | modifiers.Add(VirtualKeyCode.MENU); 98 | } 99 | 100 | inputSimulator.Keyboard.ModifiedKeyStroke(modifiers, virtualKey); 101 | } 102 | 103 | private async Task SendAsync(Action action) 104 | { 105 | await TestServices.VisualStudio.ActivateMainWindowAsync(); 106 | await Task.Run(() => action(new InputSimulator())); 107 | await Task.Yield(); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Harness/ShiftState.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | 5 | namespace EditorGuidelinesTests.Harness 6 | { 7 | [Flags] 8 | public enum ShiftState 9 | { 10 | None = 0, 11 | Shift = 1 << 0, 12 | Ctrl = 1 << 1, 13 | Alt = 1 << 2, 14 | Hankaku = 1 << 3, 15 | Reserved1 = 1 << 4, 16 | Reserved2 = 1 << 5, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Harness/SolutionServices.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Microsoft.VisualStudio; 10 | using Microsoft.VisualStudio.Shell.Interop; 11 | using Microsoft.VisualStudio.TextManager.Interop; 12 | using Microsoft.VisualStudio.Threading; 13 | 14 | namespace EditorGuidelinesTests.Harness 15 | { 16 | internal sealed class SolutionServices : AbstractServices 17 | { 18 | public SolutionServices(TestServices testServices) 19 | : base(testServices) 20 | { 21 | } 22 | 23 | public async Task OpenSolutionAsync(string path, bool saveExistingSolutionIfExists = false) 24 | { 25 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 26 | await CloseSolutionAsync(saveExistingSolutionIfExists); 27 | 28 | var solution = (IVsSolution)ServiceProvider.GetService(typeof(SVsSolution)); 29 | ErrorHandler.ThrowOnFailure(solution.OpenSolutionFile((uint)__VSSLNOPENOPTIONS.SLNOPENOPT_Silent, path)); 30 | await Task.Yield(); 31 | } 32 | 33 | public async Task SaveSolutionAsync() 34 | { 35 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 36 | 37 | if (!await IsSolutionOpenAsync()) 38 | { 39 | throw new InvalidOperationException("Cannot save solution when no solution is open."); 40 | } 41 | 42 | var solution = (IVsSolution)ServiceProvider.GetService(typeof(SVsSolution)); 43 | 44 | // Make sure the director exists so the Save dialog does not appear 45 | ErrorHandler.ThrowOnFailure(solution.GetSolutionInfo(out var solutionDirectory, out _, out _)); 46 | Directory.CreateDirectory(solutionDirectory); 47 | 48 | ErrorHandler.ThrowOnFailure(solution.SaveSolutionElement((uint)__VSSLNSAVEOPTIONS.SLNSAVEOPT_ForceSave, pHier: null, docCookie: 0)); 49 | } 50 | 51 | public async Task IsSolutionOpenAsync() 52 | { 53 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 54 | 55 | var solution = (IVsSolution)ServiceProvider.GetService(typeof(SVsSolution)); 56 | ErrorHandler.ThrowOnFailure(solution.GetProperty((int)__VSPROPID.VSPROPID_IsSolutionOpen, out var isOpen)); 57 | return (bool)isOpen; 58 | } 59 | 60 | public async Task CloseSolutionAsync(bool saveIfExists = false) 61 | { 62 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 63 | 64 | var solution = (IVsSolution)ServiceProvider.GetService(typeof(SVsSolution)); 65 | if (!await IsSolutionOpenAsync()) 66 | { 67 | return; 68 | } 69 | 70 | if (saveIfExists) 71 | { 72 | await SaveSolutionAsync(); 73 | } 74 | 75 | using (var semaphore = new SemaphoreSlim(1)) 76 | using (var solutionEvents = new SolutionEvents(TestServices, solution)) 77 | { 78 | await semaphore.WaitAsync(); 79 | solutionEvents.AfterCloseSolution += HandleAfterCloseSolution; 80 | 81 | try 82 | { 83 | ErrorHandler.ThrowOnFailure(solution.CloseSolutionElement((uint)__VSSLNCLOSEOPTIONS.SLNCLOSEOPT_DeleteProject | (uint)__VSSLNSAVEOPTIONS.SLNSAVEOPT_NoSave, pHier: null, docCookie: 0)); 84 | await semaphore.WaitAsync(); 85 | } 86 | finally 87 | { 88 | solutionEvents.AfterCloseSolution -= HandleAfterCloseSolution; 89 | } 90 | 91 | // Local functions 92 | void HandleAfterCloseSolution(object sender, EventArgs e) => semaphore.Release(); 93 | } 94 | } 95 | 96 | public async Task OpenFileAsync(string projectName, string relativeFilePath) 97 | { 98 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 99 | 100 | var filePath = GetAbsolutePathForProjectRelativeFilePath(projectName, relativeFilePath); 101 | if (!IsDocumentOpen(filePath, VSConstants.LOGVIEWID.Code_guid, out _, out _, out var windowFrame)) 102 | { 103 | var uiShellOpenDocument = (IVsUIShellOpenDocument)ServiceProvider.GetService(typeof(SVsUIShellOpenDocument)); 104 | ErrorHandler.ThrowOnFailure(uiShellOpenDocument.OpenDocumentViaProject(filePath, VSConstants.LOGVIEWID.Code_guid, out _, out _, out _, out windowFrame)); 105 | } 106 | else if (windowFrame is object) 107 | { 108 | var uiShellOpenDocument = (IVsUIShellOpenDocument3)ServiceProvider.GetService(typeof(SVsUIShellOpenDocument)); 109 | if (((__VSNEWDOCUMENTSTATE)uiShellOpenDocument.NewDocumentState).HasFlag(__VSNEWDOCUMENTSTATE.NDS_Permanent)) 110 | { 111 | windowFrame.SetProperty((int)__VSFPROPID5.VSFPROPID_IsProvisional, false); 112 | } 113 | } 114 | 115 | if (windowFrame is object) 116 | { 117 | ErrorHandler.ThrowOnFailure(windowFrame.Show()); 118 | } 119 | 120 | var view = GetTextView(windowFrame); 121 | 122 | // Reliably set focus using NavigateToLineAndColumn 123 | var textManager = (IVsTextManager)ServiceProvider.GetService(typeof(SVsTextManager)); 124 | ErrorHandler.ThrowOnFailure(view.GetBuffer(out var textLines)); 125 | ErrorHandler.ThrowOnFailure(view.GetCaretPos(out var line, out var column)); 126 | ErrorHandler.ThrowOnFailure(textManager.NavigateToLineAndColumn(textLines, VSConstants.LOGVIEWID.Code_guid, line, column, line, column)); 127 | } 128 | 129 | public async Task IsDocumentOpenAsync(string projectName, string relativeFilePath, Guid logicalView) 130 | { 131 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 132 | 133 | var filePath = GetAbsolutePathForProjectRelativeFilePath(projectName, relativeFilePath); 134 | return IsDocumentOpen(filePath, logicalView, out _, out _, out _); 135 | } 136 | 137 | private string GetAbsolutePathForProjectRelativeFilePath(string projectName, string relativeFilePath) 138 | { 139 | TestServices.ThrowIfNotOnMainThread(); 140 | 141 | var dte = (EnvDTE.DTE)ServiceProvider.GetService(typeof(EnvDTE.DTE)); 142 | var project = dte.Solution.Projects.Cast().First(x => x.Name == projectName); 143 | var projectPath = Path.GetDirectoryName(project.FullName); 144 | return Path.Combine(projectPath, relativeFilePath); 145 | } 146 | 147 | private IVsTextView GetTextView(IVsWindowFrame windowFrame) 148 | { 149 | TestServices.ThrowIfNotOnMainThread(); 150 | 151 | ErrorHandler.ThrowOnFailure(windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out var docView)); 152 | var textView = docView as IVsTextView; 153 | if (textView is null) 154 | { 155 | if (docView is IVsCodeWindow codeWindow) 156 | { 157 | ErrorHandler.ThrowOnFailure(codeWindow.GetPrimaryView(out textView)); 158 | } 159 | } 160 | 161 | return textView; 162 | } 163 | 164 | private bool IsDocumentOpen(string fullPath, Guid logicalView, out IVsUIHierarchy hierarchy, out uint itemID, out IVsWindowFrame windowFrame) 165 | { 166 | TestServices.ThrowIfNotOnMainThread(); 167 | 168 | var uiShellOpenDocument = (IVsUIShellOpenDocument)ServiceProvider.GetService(typeof(SVsUIShellOpenDocument)); 169 | var runningDocumentTable = (IVsRunningDocumentTable)ServiceProvider.GetService(typeof(SVsRunningDocumentTable)); 170 | 171 | var docData = IntPtr.Zero; 172 | try 173 | { 174 | var itemidOpen = new uint[1]; 175 | ErrorHandler.ThrowOnFailure(runningDocumentTable.FindAndLockDocument((uint)_VSRDTFLAGS.RDT_NoLock, fullPath, out var hier, out itemidOpen[0], out docData, out var cookie)); 176 | 177 | var flags = logicalView == Guid.Empty ? (uint)__VSIDOFLAGS.IDO_IgnoreLogicalView : 0; 178 | ErrorHandler.ThrowOnFailure(uiShellOpenDocument.IsDocumentOpen((IVsUIHierarchy)hier, itemidOpen[0], fullPath, logicalView, flags, out hierarchy, itemidOpen, out windowFrame, out var open)); 179 | if (windowFrame is object) 180 | { 181 | itemID = itemidOpen[0]; 182 | return open == 1; 183 | } 184 | } 185 | finally 186 | { 187 | if (docData != IntPtr.Zero) 188 | { 189 | Marshal.Release(docData); 190 | } 191 | } 192 | 193 | itemID = (uint)VSConstants.VSITEMID.Nil; 194 | return false; 195 | } 196 | 197 | private sealed class SolutionEvents : IVsSolutionEvents, IDisposable 198 | { 199 | private readonly JoinableTaskFactory _joinableTaskFactory; 200 | private readonly IVsSolution _solution; 201 | private readonly uint _cookie; 202 | 203 | public SolutionEvents(TestServices testServices, IVsSolution solution) 204 | { 205 | testServices.ThrowIfNotOnMainThread(); 206 | 207 | _joinableTaskFactory = testServices.JoinableTaskFactory; 208 | _solution = solution; 209 | ErrorHandler.ThrowOnFailure(solution.AdviseSolutionEvents(this, out _cookie)); 210 | } 211 | 212 | public event EventHandler AfterCloseSolution; 213 | 214 | public void Dispose() 215 | { 216 | _joinableTaskFactory.Run(async () => 217 | { 218 | await _joinableTaskFactory.SwitchToMainThreadAsync(); 219 | ErrorHandler.ThrowOnFailure(_solution.UnadviseSolutionEvents(_cookie)); 220 | }); 221 | } 222 | 223 | public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) => VSConstants.S_OK; 224 | public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) => VSConstants.S_OK; 225 | public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) => VSConstants.S_OK; 226 | public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) => VSConstants.S_OK; 227 | public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) => VSConstants.S_OK; 228 | public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) => VSConstants.S_OK; 229 | public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution) => VSConstants.S_OK; 230 | public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) => VSConstants.S_OK; 231 | public int OnBeforeCloseSolution(object pUnkReserved) => VSConstants.S_OK; 232 | 233 | public int OnAfterCloseSolution(object pUnkReserved) 234 | { 235 | AfterCloseSolution?.Invoke(this, EventArgs.Empty); 236 | return VSConstants.S_OK; 237 | } 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Harness/TestServices.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using System.Threading; 6 | using Microsoft; 7 | using Microsoft.VisualStudio.Threading; 8 | 9 | namespace EditorGuidelinesTests.Harness 10 | { 11 | public sealed class TestServices 12 | { 13 | internal TestServices(JoinableTaskFactory joinableTaskFactory, IServiceProvider serviceProvider) 14 | { 15 | JoinableTaskFactory = joinableTaskFactory; 16 | ServiceProvider = serviceProvider; 17 | 18 | SendInput = new SendInputServices(this); 19 | Solution = new SolutionServices(this); 20 | VisualStudio = new VisualStudioServices(this); 21 | } 22 | 23 | public JoinableTaskFactory JoinableTaskFactory { get; } 24 | public IServiceProvider ServiceProvider { get; } 25 | 26 | public TimeSpan HangMitigatingTimeout 27 | { 28 | get 29 | { 30 | if (Debugger.IsAttached) 31 | { 32 | return Timeout.InfiniteTimeSpan; 33 | } 34 | 35 | return TimeSpan.FromMinutes(1); 36 | } 37 | } 38 | 39 | internal SendInputServices SendInput { get; } 40 | internal SolutionServices Solution { get; } 41 | internal VisualStudioServices VisualStudio { get; } 42 | 43 | internal void ThrowIfNotOnMainThread() 44 | { 45 | Verify.Operation(JoinableTaskFactory.Context.MainThread == Thread.CurrentThread, "This type can only be constructed on the main thread."); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Harness/VisualStudioServices.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using System.Threading.Tasks; 6 | 7 | namespace EditorGuidelinesTests.Harness 8 | { 9 | internal sealed class VisualStudioServices : AbstractServices 10 | { 11 | public VisualStudioServices(TestServices testServices) 12 | : base(testServices) 13 | { 14 | } 15 | 16 | public async Task ActivateMainWindowAsync() 17 | { 18 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 19 | 20 | var dte = (EnvDTE.DTE)ServiceProvider.GetService(typeof(EnvDTE.DTE)); 21 | var activeWindow = (IntPtr)dte.ActiveWindow.HWnd; 22 | if (activeWindow == IntPtr.Zero) 23 | { 24 | activeWindow = (IntPtr)dte.MainWindow.HWnd; 25 | } 26 | 27 | SetForegroundWindow(activeWindow); 28 | } 29 | 30 | private void SetForegroundWindow(IntPtr window) 31 | { 32 | TestServices.ThrowIfNotOnMainThread(); 33 | 34 | var activeWindow = NativeMethods.GetLastActivePopup(window); 35 | activeWindow = NativeMethods.IsWindowVisible(activeWindow) ? activeWindow : window; 36 | NativeMethods.SwitchToThisWindow(activeWindow, true); 37 | 38 | if (!NativeMethods.SetForegroundWindow(activeWindow)) 39 | { 40 | if (!NativeMethods.AllocConsole()) 41 | { 42 | Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); 43 | } 44 | 45 | try 46 | { 47 | var consoleWindow = NativeMethods.GetConsoleWindow(); 48 | if (consoleWindow == IntPtr.Zero) 49 | { 50 | throw new InvalidOperationException("Failed to obtain the console window."); 51 | } 52 | 53 | if (!NativeMethods.SetWindowPos(consoleWindow, IntPtr.Zero, 0, 0, 0, 0, NativeMethods.SWP_NOZORDER)) 54 | { 55 | Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); 56 | } 57 | } 58 | finally 59 | { 60 | if (!NativeMethods.FreeConsole()) 61 | { 62 | Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); 63 | } 64 | } 65 | 66 | if (!NativeMethods.SetForegroundWindow(activeWindow)) 67 | { 68 | throw new InvalidOperationException("Failed to set the foreground window."); 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/OpenFileTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using EditorGuidelinesTests.Harness; 8 | using Microsoft.VisualStudio; 9 | using WindowsInput.Native; 10 | using Xunit; 11 | 12 | namespace EditorGuidelinesTests 13 | { 14 | [VsTestSettings(ReuseInstance = false, Version = "[15.0-)")] 15 | public class OpenFirstFileTests : AbstractIntegrationTest 16 | { 17 | private string _testSolutionDirectory; 18 | private string _testSolutionFile; 19 | private string _testProjectFile; 20 | private string _testConfigurationFile; 21 | private string _testSourceFile; 22 | 23 | public override async Task InitializeAsync() 24 | { 25 | await base.InitializeAsync(); 26 | 27 | _testSolutionDirectory = Path.Combine(Path.GetTempPath(), nameof(EditorGuidelinesTests), nameof(OpenFirstFileTests), Path.GetRandomFileName()); 28 | Directory.CreateDirectory(_testSolutionDirectory); 29 | 30 | var solutionGuid = Guid.NewGuid(); 31 | var projectGuid = Guid.NewGuid(); 32 | _testSolutionFile = Path.Combine(_testSolutionDirectory, "Test.sln"); 33 | File.WriteAllText(_testSolutionFile, $@" 34 | Microsoft Visual Studio Solution File, Format Version 12.00 35 | # Visual Studio Version 16 36 | VisualStudioVersion = 16.0.28729.10 37 | MinimumVisualStudioVersion = 10.0.40219.1 38 | Project(""{{9A19103F-16F7-4668-BE54-9A1E7A4F7556}}"") = ""Test"", ""Test.csproj"", ""{projectGuid:B}"" 39 | EndProject 40 | Global 41 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 42 | Debug|Any CPU = Debug|Any CPU 43 | Release|Any CPU = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 46 | {projectGuid:B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {projectGuid:B}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {projectGuid:B}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {projectGuid:B}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(ExtensibilityGlobals) = postSolution 55 | SolutionGuid = {solutionGuid:B} 56 | EndGlobalSection 57 | EndGlobal 58 | "); 59 | _testProjectFile = Path.Combine(_testSolutionDirectory, "Test.csproj"); 60 | File.WriteAllText(_testProjectFile, @" 61 | 62 | 63 | 64 | netstandard2.0 65 | 66 | 67 | 68 | "); 69 | _testConfigurationFile = Path.Combine(_testSolutionDirectory, ".editorconfig"); 70 | File.WriteAllText(_testConfigurationFile, @" 71 | root = true 72 | 73 | [*] 74 | guidelines = 120 75 | "); 76 | _testSourceFile = Path.Combine(_testSolutionDirectory, "Class1.cs"); 77 | File.WriteAllText(_testSourceFile, @"class Class1 { 78 | } 79 | "); 80 | } 81 | 82 | public override async Task DisposeAsync() 83 | { 84 | try 85 | { 86 | await TestServices.Solution.CloseSolutionAsync(); 87 | } 88 | finally 89 | { 90 | await base.DisposeAsync(); 91 | } 92 | } 93 | 94 | public override void Dispose() 95 | { 96 | try 97 | { 98 | if (Directory.Exists(_testSolutionDirectory)) 99 | { 100 | try 101 | { 102 | Directory.Delete(_testSolutionDirectory, recursive: true); 103 | } 104 | catch 105 | { 106 | // Ignore failures. Users can clean up the folders later since they are all grouped into 107 | // %TEMP%\EditorGuidelinesTests. 108 | } 109 | } 110 | } 111 | finally 112 | { 113 | base.Dispose(); 114 | } 115 | } 116 | 117 | /// 118 | /// Verifies that opening the first file via Visual Studio APIs will not cause an exception. 119 | /// 120 | [VsFact] 121 | public async Task TestOpenFileViaAPI() 122 | { 123 | await TestServices.Solution.OpenSolutionAsync(_testSolutionFile); 124 | 125 | await TestServices.Solution.OpenFileAsync( 126 | projectName: Path.GetFileNameWithoutExtension(_testProjectFile), 127 | relativeFilePath: Path.GetFileName(_testSourceFile)); 128 | 129 | // ➡ TODO: wait for guidelines to appear and assert they are at the correct position 130 | } 131 | 132 | /// 133 | /// Verifies that opening the first file via a Navigate To operation will not cause an exception. 134 | /// 135 | [VsFact] 136 | public async Task TestOpenFileFromNavigateTo() 137 | { 138 | await TestServices.Solution.OpenSolutionAsync(_testSolutionFile); 139 | 140 | await TestServices.SendInput.SendAsync( 141 | (VirtualKeyCode.VK_T, ShiftState.Ctrl), 142 | "f Class1.cs", 143 | VirtualKeyCode.RETURN); 144 | 145 | using (var cancellationTokenSource = new CancellationTokenSource(TestServices.HangMitigatingTimeout)) 146 | { 147 | while (true) 148 | { 149 | await Task.Yield(); 150 | cancellationTokenSource.Token.ThrowIfCancellationRequested(); 151 | 152 | if (await TestServices.Solution.IsDocumentOpenAsync( 153 | projectName: Path.GetFileNameWithoutExtension(_testProjectFile), 154 | relativeFilePath: Path.GetFileName(_testSourceFile), 155 | VSConstants.LOGVIEWID.Code_guid)) 156 | { 157 | break; 158 | } 159 | } 160 | } 161 | 162 | // ➡ TODO: wait for guidelines to appear and assert they are at the correct position 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using Xunit; 4 | 5 | [assembly: CollectionBehavior(DisableTestParallelization = true)] 6 | [assembly: VsTestSettings(UIThread = true)] 7 | -------------------------------------------------------------------------------- /test/EditorGuidelinesTests/Threading/JoinableTaskFactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. 2 | 3 | using System.Threading; 4 | using System.Windows.Threading; 5 | using Microsoft; 6 | using Microsoft.VisualStudio.Threading; 7 | 8 | namespace EditorGuidelinesTests.Threading 9 | { 10 | // JoinableTaskFactory.WithPriority is available in later releases of vs-threading, but we reference 1.2.0.0 for 11 | // compatibility with Visual Studio 2013. 12 | // https://github.com/Microsoft/vs-threading/pull/142 13 | internal static class JoinableTaskFactoryExtensions 14 | { 15 | internal static JoinableTaskFactory WithPriority(this JoinableTaskFactory joinableTaskFactory, Dispatcher dispatcher, DispatcherPriority priority) 16 | { 17 | Requires.NotNull(joinableTaskFactory, nameof(joinableTaskFactory)); 18 | Requires.NotNull(dispatcher, nameof(dispatcher)); 19 | 20 | return new DispatcherJoinableTaskFactory(joinableTaskFactory, dispatcher, priority); 21 | } 22 | 23 | private class DispatcherJoinableTaskFactory : DelegatingJoinableTaskFactory 24 | { 25 | private readonly Dispatcher _dispatcher; 26 | private readonly DispatcherPriority _priority; 27 | 28 | public DispatcherJoinableTaskFactory(JoinableTaskFactory innerFactory, Dispatcher dispatcher, DispatcherPriority priority) 29 | : base(innerFactory) 30 | { 31 | _dispatcher = dispatcher; 32 | _priority = priority; 33 | } 34 | 35 | protected override void PostToUnderlyingSynchronizationContext(SendOrPostCallback callback, object state) 36 | { 37 | _dispatcher.BeginInvoke(_priority, callback, state); 38 | } 39 | } 40 | } 41 | } 42 | --------------------------------------------------------------------------------