Skip to content

Commit 967065e

Browse files
committed
Add virtual document abstraction
wip wip
1 parent e03cbba commit 967065e

File tree

3 files changed

+150
-1
lines changed

3 files changed

+150
-1
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { Position, TextDocument } from 'vscode-languageserver-textdocument'
2+
import type { State } from '../util/state'
3+
import { LanguageBoundary } from '../util/getLanguageBoundaries'
4+
import { isWithinRange } from '../util/isWithinRange'
5+
import { getTextWithoutComments } from '../util/doc'
6+
7+
export type DocumentContext = 'html' | 'css' | 'js' | 'other' | (string & {})
8+
9+
export class VirtualDocument {
10+
/**
11+
* A getter for the global state of the server. This state is shared across
12+
* all virtual documents and may change arbitrarily over time.
13+
*
14+
* This is a getter to prevent stale references from being held.
15+
*/
16+
private state: () => State
17+
18+
/**
19+
* The "text document" that contains the content of this document and all of
20+
* its embedded documents.
21+
*/
22+
private storage: TextDocument
23+
24+
/**
25+
* Conseptual boundaries of the document where different languages are used
26+
*
27+
* This is used to determine how the document is structured and what parts
28+
* are relevant to the current operation.
29+
*/
30+
private boundaries: LanguageBoundary[]
31+
32+
constructor(
33+
state: () => State,
34+
storage: TextDocument,
35+
boundaries: LanguageBoundary[],
36+
) {
37+
this.state = state
38+
this.storage = storage
39+
this.boundaries = boundaries
40+
}
41+
42+
/**
43+
* The current content of the document
44+
*/
45+
public get contents() {
46+
// Replace all comment nodes with whitespace
47+
let tmp = ''
48+
49+
let type = this.boundaries[0].type
50+
if (type === 'html') {
51+
tmp = getTextWithoutComments(this.storage, 'html')
52+
} else if (type === 'css') {
53+
tmp = getTextWithoutComments(this.storage, 'css')
54+
} else if (type === 'js') {
55+
tmp = getTextWithoutComments(this.storage, 'js')
56+
} else if (type === 'jsx') {
57+
tmp = getTextWithoutComments(this.storage, 'jsx')
58+
}
59+
60+
return tmp
61+
}
62+
63+
/**
64+
* Find the inner-most scope containing a given cursor position.
65+
*/
66+
public boundaryAt(cursor: Position): LanguageBoundary | null {
67+
for (let boundary of this.boundaries) {
68+
if (isWithinRange(cursor, boundary.range)) {
69+
return boundary
70+
}
71+
}
72+
73+
return null
74+
}
75+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { TextDocument } from 'vscode-languageserver-textdocument'
2+
import type { State } from '../util/state'
3+
import { VirtualDocument } from './document'
4+
import { analyzeDocument } from '../scopes/analyze'
5+
import { getDocumentLanguages, getLanguageBoundaries } from '../util/getLanguageBoundaries'
6+
7+
export class DocumentManager {
8+
/**
9+
* A getter for the global state of the server. This state is shared across
10+
* all virtual documents and may change arbitrarily over time.
11+
12+
* This is a getter to prevent stale references from being held.
13+
*/
14+
private state: () => State
15+
16+
/**
17+
* A map of document URIs to their respective virtual documents.
18+
*/
19+
private store = new Map<string, VirtualDocument>()
20+
21+
constructor(state: () => State) {
22+
this.state = state
23+
}
24+
25+
get(uri: TextDocument): Promise<VirtualDocument>
26+
get(uri: string): Promise<VirtualDocument | null>
27+
28+
async get(doc: TextDocument | string): Promise<VirtualDocument | null> {
29+
if (typeof doc === 'string') {
30+
return this.store.get(doc) ?? null
31+
}
32+
33+
let found = this.store.get(doc.uri)
34+
if (found) return found
35+
36+
let state = this.state()
37+
let scopes = await analyzeDocument(state, doc)
38+
let boundaries = getDocumentLanguages(state, doc)
39+
found = new VirtualDocument(this.state, doc, boundaries, scopes)
40+
this.store.set(doc.uri, found)
41+
42+
return found
43+
}
44+
}

packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { isJsDoc } from './js'
77
import moo, { Rules } from 'moo'
88
import Cache from 'tmp-cache'
99
import { getTextWithoutComments } from './doc'
10-
import { isCssLanguage } from './css'
10+
import { isCssDoc, isCssLanguage } from './css'
1111

1212
export type LanguageBoundary = {
1313
type: 'html' | 'js' | 'jsx' | 'css' | (string & {})
@@ -146,6 +146,36 @@ export function clearLanguageBoundariesCache() {
146146
cache.clear()
147147
}
148148

149+
/**
150+
* @deprecated
151+
*/
152+
export function getDocumentLanguages(state: State, doc: TextDocument): LanguageBoundary[] {
153+
let boundaries = getLanguageBoundaries(state, doc)
154+
if (boundaries) return boundaries
155+
156+
// If we get here we most likely have non-HTML document in a single language
157+
let type = doc.languageId
158+
159+
if (isCssDoc(state, doc)) {
160+
type = 'css'
161+
} else if (isHtmlDoc(state, doc)) {
162+
type = 'html'
163+
} else if (isJsDoc(state, doc)) {
164+
type = 'js'
165+
}
166+
167+
let text = doc.getText()
168+
169+
return [
170+
{
171+
type,
172+
lang: doc.languageId,
173+
span: [0, text.length],
174+
range: { start: doc.positionAt(0), end: doc.positionAt(text.length) },
175+
},
176+
]
177+
}
178+
149179
export function getLanguageBoundaries(
150180
state: State,
151181
doc: TextDocument,

0 commit comments

Comments
 (0)