Skip to content

Commit 50c8940

Browse files
committed
Add debug UI for viewing document scopes
1 parent 06cfc37 commit 50c8940

File tree

4 files changed

+272
-3
lines changed

4 files changed

+272
-3
lines changed

packages/tailwindcss-language-server/src/tw.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import { ProjectLocator, type ProjectConfig } from './project-locator'
4848
import type { TailwindCssSettings } from '@tailwindcss/language-service/src/util/state'
4949
import { createResolver, Resolver } from './resolver'
5050
import { retry } from './util/retry'
51+
import { analyzeDocument } from '@tailwindcss/language-service/src/scopes/analyze'
52+
import type { AnyScope } from '@tailwindcss/language-service/src/scopes/scope'
5153

5254
const TRIGGER_CHARACTERS = [
5355
// class attributes
@@ -764,12 +766,17 @@ export class TW {
764766
private onRequest(
765767
method: '@/tailwindCSS/sortSelection',
766768
params: { uri: string; classLists: string[] },
767-
): { error: string } | { classLists: string[] }
769+
): Promise<{ error: string } | { classLists: string[] }>
768770
private onRequest(
769771
method: '@/tailwindCSS/getProject',
770772
params: { uri: string },
771-
): { version: string } | null
772-
private onRequest(method: string, params: any): any {
773+
): Promise<{ version: string } | null>
774+
private onRequest(
775+
method: '@/tailwindCSS/scopes/get',
776+
params: { uri: string },
777+
): Promise<{ scopes: AnyScope[] } | null>
778+
779+
private async onRequest(method: string, params: any): Promise<any> {
773780
if (method === '@/tailwindCSS/sortSelection') {
774781
let project = this.getProject({ uri: params.uri })
775782
if (!project) {
@@ -791,6 +798,24 @@ export class TW {
791798
version: project.state.version,
792799
}
793800
}
801+
802+
if (method === '@/tailwindCSS/scopes/get') {
803+
console.log('Get scopes plz')
804+
805+
let doc = this.documentService.getDocument(params.uri)
806+
if (!doc) return { scopes: [] }
807+
808+
let project = this.getProject({ uri: params.uri })
809+
if (!project) return { scopes: [] }
810+
if (!project.enabled()) return { scopes: [] }
811+
if (!project.state.enabled) return { scopes: [] }
812+
813+
let tree = await analyzeDocument(project.state, doc)
814+
815+
return {
816+
scopes: tree.all(),
817+
}
818+
}
794819
}
795820

796821
private updateCapabilities() {

packages/vscode-tailwindcss/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,14 @@
334334
"description": "Traces the communication between VS Code and the Tailwind CSS Language Server."
335335
}
336336
}
337+
},
338+
"views": {
339+
"explorer": [
340+
{
341+
"id": "scopes",
342+
"name": "Tailwind CSS Scopes"
343+
}
344+
]
337345
}
338346
},
339347
"scripts": {

packages/vscode-tailwindcss/src/extension.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { CONFIG_GLOB, CSS_GLOB } from '@tailwindcss/language-server/src/lib/cons
3737
import braces from 'braces'
3838
import normalizePath from 'normalize-path'
3939
import * as servers from './servers/index'
40+
import { registerScopeProvider } from './scopes'
4041

4142
const colorNames = Object.keys(namedColors)
4243

@@ -186,6 +187,12 @@ export async function activate(context: ExtensionContext) {
186187

187188
await commands.executeCommand('setContext', 'tailwindCSS.hasOutputChannel', true)
188189

190+
registerScopeProvider({
191+
get client() {
192+
return currentClient
193+
},
194+
})
195+
189196
outputChannel.appendLine(`Locating server…`)
190197

191198
let module = context.asAbsolutePath(path.join('dist', 'server.js'))
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import * as vscode from 'vscode'
2+
import { ScopeTree } from '@tailwindcss/language-service/src/scopes/tree'
3+
import type { AnyScope } from '@tailwindcss/language-service/src/scopes/scope'
4+
import { LanguageClient } from 'vscode-languageclient/node'
5+
6+
interface ScopeOptions {
7+
tree: ScopeTree
8+
cursor: number
9+
}
10+
11+
interface AnySource {
12+
name: string
13+
span: [number, number] | null | undefined
14+
}
15+
16+
type TreeData = AnyScope | AnySource
17+
18+
class ScopeProvider implements vscode.TreeDataProvider<TreeData> {
19+
private tree: ScopeTree
20+
private cursor: number
21+
22+
constructor() {
23+
this.tree = new ScopeTree([])
24+
}
25+
26+
getParent(element: TreeData): vscode.ProviderResult<TreeData> {
27+
if ('name' in element) {
28+
if (!element.span) return null
29+
30+
let path = this.tree.at(element.span[0])
31+
let parent = path.at(-1)
32+
if (!parent) return null
33+
34+
return parent
35+
}
36+
37+
let path = this.tree.pathTo(element)
38+
let index = path.indexOf(element)
39+
if (index === -1) return null
40+
41+
let parent = path[index - 1]
42+
if (!parent) return null
43+
44+
return parent
45+
}
46+
47+
getTreeItem(scope: TreeData): vscode.TreeItem {
48+
if ('name' in scope) {
49+
return new SourceItem(scope)
50+
}
51+
52+
let isOpen = scope.source.scope[0] <= this.cursor && this.cursor <= scope.source.scope[1]
53+
54+
let state =
55+
scope.children.length > 0
56+
? isOpen
57+
? vscode.TreeItemCollapsibleState.Expanded
58+
: vscode.TreeItemCollapsibleState.Collapsed
59+
: vscode.TreeItemCollapsibleState.None
60+
61+
return new ScopeItem(scope, state)
62+
}
63+
64+
async getChildren(element?: TreeData): Promise<TreeData[]> {
65+
if (!element) {
66+
return this.tree.all()
67+
}
68+
69+
if ('name' in element) return []
70+
71+
let children: TreeData[] = []
72+
73+
for (let [name, span] of Object.entries(element.source)) {
74+
if (name === 'scope') continue
75+
children.push({ name, span })
76+
}
77+
78+
children.push(...element.children)
79+
80+
return children
81+
}
82+
83+
update(options: Partial<ScopeOptions>) {
84+
this.tree = options.tree ?? this.tree
85+
this.cursor = options.cursor ?? this.cursor
86+
this._onDidChangeTreeData.fire()
87+
}
88+
89+
private _onDidChangeTreeData: vscode.EventEmitter<AnyScope | undefined | null | void> =
90+
new vscode.EventEmitter<AnyScope | undefined | null | void>()
91+
readonly onDidChangeTreeData: vscode.Event<AnyScope | undefined | null | void> =
92+
this._onDidChangeTreeData.event
93+
}
94+
95+
class ScopeItem extends vscode.TreeItem {
96+
constructor(scope: AnyScope, state: vscode.TreeItemCollapsibleState) {
97+
let label: vscode.TreeItemLabel = {
98+
label: `${scope.kind} [${scope.source.scope[0]}-${scope.source.scope[1]}]`,
99+
}
100+
101+
super(label, state)
102+
this.iconPath = new vscode.ThemeIcon('code')
103+
this.tooltip = new vscode.MarkdownString(
104+
`\`\`\`json\n${JSON.stringify({ ...scope, children: undefined }, null, 2)}\n\`\`\``,
105+
)
106+
}
107+
}
108+
109+
class SourceItem extends vscode.TreeItem {
110+
constructor(source: AnySource) {
111+
let label: vscode.TreeItemLabel = {
112+
label: `- ${source.name}: `,
113+
}
114+
115+
if (source.span) {
116+
label.label += `[${source.span[0]}-${source.span[1]}]`
117+
} else {
118+
label.label += '(none)'
119+
}
120+
121+
super(label, vscode.TreeItemCollapsibleState.None)
122+
this.iconPath = new vscode.ThemeIcon('code')
123+
}
124+
}
125+
126+
interface ScopeProviderOptions {
127+
readonly client: Promise<LanguageClient> | null
128+
}
129+
130+
export function registerScopeProvider(opts: ScopeProviderOptions): vscode.Disposable {
131+
let trees: Map<string, ScopeTree> = new Map()
132+
let emptyTree = new ScopeTree([])
133+
let scopeProvider = new ScopeProvider()
134+
let disposables: vscode.Disposable[] = []
135+
136+
let treeView = vscode.window.createTreeView('scopes', {
137+
treeDataProvider: scopeProvider,
138+
})
139+
140+
disposables.push(treeView)
141+
142+
vscode.workspace.onDidChangeTextDocument(
143+
async (event) => {
144+
if (event.document !== vscode.window.activeTextEditor.document) return
145+
if (!opts.client) return
146+
147+
let client = await opts.client
148+
149+
interface ScopesGetResponse {
150+
scopes: AnyScope[]
151+
}
152+
153+
let response = await client.sendRequest<ScopesGetResponse>('@/tailwindCSS/scopes/get', {
154+
uri: event.document.uri.toString(),
155+
})
156+
157+
let tree = new ScopeTree(response.scopes)
158+
trees.set(event.document.uri.toString(), tree)
159+
160+
await refresh()
161+
},
162+
null,
163+
disposables,
164+
)
165+
166+
vscode.window.onDidChangeActiveTextEditor(
167+
async () => {
168+
await refresh()
169+
},
170+
null,
171+
disposables,
172+
)
173+
174+
vscode.window.onDidChangeTextEditorSelection(
175+
async (event) => {
176+
if (event.textEditor !== vscode.window.activeTextEditor) return
177+
178+
let editor = event.textEditor
179+
let cursor = editor.document.offsetAt(editor.selection.active)
180+
let tree = trees.get(editor.document.uri.toString()) ?? emptyTree
181+
let scope = tree.at(cursor).at(-1)
182+
183+
if (scope) {
184+
treeView.reveal(scope, {
185+
// select: false,
186+
// focus: true,
187+
expand: true,
188+
})
189+
}
190+
},
191+
null,
192+
disposables,
193+
)
194+
195+
async function refresh() {
196+
if (!opts.client) return
197+
198+
let editor = vscode.window.activeTextEditor
199+
let cursor = editor.document.offsetAt(editor.selection.active)
200+
201+
scopeProvider.update({
202+
tree: trees.get(editor.document.uri.toString()) ?? emptyTree,
203+
cursor,
204+
})
205+
}
206+
207+
let decoration = vscode.window.createTextEditorDecorationType({
208+
backgroundColor: 'rgba(0, 0, 0, 0.1)',
209+
})
210+
211+
disposables.push(decoration)
212+
213+
function decorationForScope(scope: AnyScope) {
214+
let depth = 0
215+
for (let tree of trees.values()) {
216+
let path = tree.pathTo(scope)
217+
if (path.length > 0) {
218+
depth = path.length
219+
break
220+
}
221+
}
222+
223+
return decoration
224+
}
225+
226+
return new vscode.Disposable(() => {
227+
disposables.forEach((disposable) => disposable.dispose())
228+
})
229+
}

0 commit comments

Comments
 (0)