├── .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 | [](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 | [](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview)
10 | [](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview)
11 | [](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelinesPreview)
12 |
13 | #### Visual Studio 2015, 2017, 2019
14 | [](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines)
15 | [](https://marketplace.visualstudio.com/items?itemName=PaulHarrington.EditorGuidelines)
16 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
50 |
51 |
54 |
57 |
58 |
59 |
60 |
61 |
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 |
131 |
132 |
134 |
137 |
138 |
139 |
142 |
143 |
144 |
147 |
148 |
149 |
152 |
153 |
154 |
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 |
19 |
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 |
--------------------------------------------------------------------------------