1+ # Example in VSCode tasks.json (you can use either pwsh.exe or powershell.exe depending on your preference and setup)
2+ # {
3+ # "label": "XML: Copy absolute element path",
4+ # "type": "process",
5+ # "command": "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
6+ # "args": [
7+ # "-NoProfile",
8+ # "-NonInteractive",
9+ # "-File",
10+ # "your\\path\\to\\copy_xpath.ps1",
11+ # "-File",
12+ # "${file}",
13+ # "-Line",
14+ # "${lineNumber}",
15+ # "-Column",
16+ # "${columnNumber}"
17+ # ],
18+ # "problemMatcher": [],
19+ # "presentation": {
20+ # "reveal": "never",
21+ # "panel": "shared"
22+ # }
23+ # }
24+
25+ # Example in VSCode keybindings.json
26+ # {
27+ # "key": "ctrl+alt+x",
28+ # "command": "workbench.action.tasks.runTask",
29+ # "args": "XML: Copy absolute element path",
30+ # "when": "editorTextFocus"
31+ # }
32+
33+ param (
34+ [Parameter (Mandatory = $true )]
35+ [string ]$File ,
36+
37+ [Parameter (Mandatory = $true )]
38+ [int ]$Line ,
39+
40+ [Parameter (Mandatory = $true )]
41+ [int ]$Column ,
42+
43+ [switch ]$IncludeIndexes
44+ )
45+
46+ Set-StrictMode - Version Latest
47+ $ErrorActionPreference = ' Stop'
48+
49+ function Compare-Position {
50+ param (
51+ [int ]$LineA ,
52+ [int ]$ColA ,
53+ [int ]$LineB ,
54+ [int ]$ColB
55+ )
56+
57+ if ($LineA -lt $LineB ) { return -1 }
58+ if ($LineA -gt $LineB ) { return 1 }
59+ if ($ColA -lt $ColB ) { return -1 }
60+ if ($ColA -gt $ColB ) { return 1 }
61+ return 0
62+ }
63+
64+ function Get-SiblingIndex {
65+ param (
66+ [System.Collections.ArrayList ]$Stack ,
67+ [string ]$Name
68+ )
69+
70+ if ($Stack.Count -eq 0 ) {
71+ return 1
72+ }
73+
74+ $parent = $Stack [$Stack.Count - 1 ]
75+ if (-not $parent.ContainsKey (' ChildCounters' )) {
76+ $parent [' ChildCounters' ] = @ {}
77+ }
78+
79+ if (-not $parent [' ChildCounters' ].ContainsKey($Name )) {
80+ $parent [' ChildCounters' ][$Name ] = 0
81+ }
82+
83+ $parent [' ChildCounters' ][$Name ]++
84+ return [int ]$parent [' ChildCounters' ][$Name ]
85+ }
86+
87+ if (-not (Test-Path - LiteralPath $File )) {
88+ throw (" File not found: {0}" -f $File )
89+ }
90+
91+ $settings = [System.Xml.XmlReaderSettings ]::new()
92+ $settings.DtdProcessing = [System.Xml.DtdProcessing ]::Ignore
93+ $settings.IgnoreComments = $false
94+ $settings.IgnoreProcessingInstructions = $false
95+ $settings.IgnoreWhitespace = $false
96+
97+ $stack = [System.Collections.ArrayList ]::new()
98+ $bestPath = $null
99+
100+ $reader = [System.Xml.XmlReader ]::Create($File , $settings )
101+
102+ try {
103+ while ($reader.Read ()) {
104+ $lineInfo = [System.Xml.IXmlLineInfo ]$reader
105+ $nodeLine = if ($lineInfo.HasLineInfo ()) { $lineInfo.LineNumber } else { 0 }
106+ $nodeCol = if ($lineInfo.HasLineInfo ()) { $lineInfo.LinePosition } else { 0 }
107+
108+ $cmp = Compare-Position - LineA $nodeLine - ColA $nodeCol - LineB $Line - ColB $Column
109+
110+ switch ($reader.NodeType ) {
111+ ([System.Xml.XmlNodeType ]::Element) {
112+ if ($cmp -gt 0 ) {
113+ break
114+ }
115+
116+ $index = Get-SiblingIndex - Stack $stack - Name $reader.Name
117+
118+ $entry = @ {
119+ Name = $reader.Name
120+ Index = $index
121+ StartLine = $nodeLine
122+ StartColumn = $nodeCol
123+ ChildCounters = @ {}
124+ }
125+
126+ [void ]$stack.Add ($entry )
127+
128+ $bestPath = @ ($stack | ForEach-Object { $_ })
129+
130+ if ($reader.IsEmptyElement ) {
131+ [void ]$stack.RemoveAt ($stack.Count - 1 )
132+ }
133+ }
134+
135+ ([System.Xml.XmlNodeType ]::EndElement) {
136+ if ($cmp -gt 0 ) {
137+ break
138+ }
139+
140+ if ($stack.Count -gt 0 ) {
141+ [void ]$stack.RemoveAt ($stack.Count - 1 )
142+ }
143+
144+ $bestPath = @ ($stack | ForEach-Object { $_ })
145+ }
146+
147+ default {
148+ if ($cmp -gt 0 ) {
149+ break
150+ }
151+
152+ $bestPath = @ ($stack | ForEach-Object { $_ })
153+ }
154+ }
155+ }
156+ }
157+ finally {
158+ $reader.Dispose ()
159+ }
160+
161+ if (-not $bestPath -or $bestPath.Count -eq 0 ) {
162+ throw (" Could not determine XML element for cursor position {0}:{1}" -f $Line , $Column )
163+ }
164+
165+ $parts = foreach ($item in $bestPath ) {
166+ if ($IncludeIndexes ) {
167+ ' {0}[{1}]' -f $item.Name , $item.Index
168+ }
169+ else {
170+ $item.Name
171+ }
172+ }
173+
174+ $result = $parts -join ' ->'
175+
176+ try {
177+ Set-Clipboard - Value $result
178+ }
179+ catch {
180+ Write-Warning (" Could not copy to clipboard automatically: {0}" -f $_.Exception.Message )
181+ }
182+
183+ Write-Output $result
0 commit comments