Skip to content

Commit a88ebd5

Browse files
committed
feat: Code completion and definitions for per-block namespace syntax.
1 parent 146b71d commit a88ebd5

File tree

7 files changed

+157
-73
lines changed

7 files changed

+157
-73
lines changed

packages/@css-blocks/language-server/src/serverCapabilities.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const SERVER_CAPABILITIES: ServerCapabilities = {
77
// hoverProvider: true,
88
documentSymbolProvider: false,
99
completionProvider: {
10-
resolveProvider: true,
10+
resolveProvider: false,
11+
"triggerCharacters": [ ':', '"', "=" ],
1112
},
1213
};

packages/@css-blocks/language-server/src/util/hbsCompletionProvider.ts

+47-30
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Block, BlockFactory } from "@css-blocks/core/dist/src";
1+
import { Block, BlockFactory } from "@css-blocks/core";
22
import { CompletionItem, CompletionItemKind, Position, TextDocument } from "vscode-languageserver-types";
33

44
import { PathTransformer } from "../pathTransformers/PathTransformer";
55

6-
import { getItemAtCursor } from "./hbsUtils";
6+
import { getItemAtCursor, AttributeType, ClassAttribute } from "./hbsUtils";
77
import { transformPathsFromUri } from "./pathTransformer";
88

99
export async function getHbsCompletions(document: TextDocument, position: Position, blockFactory: BlockFactory, pathTransformer: PathTransformer): Promise<CompletionItem[]> {
@@ -22,15 +22,32 @@ export async function getHbsCompletions(document: TextDocument, position: Positi
2222
return [];
2323
}
2424

25-
if (itemAtCursor.referencedBlock) {
26-
block = block.getExportedBlock(itemAtCursor.referencedBlock);
25+
let attributeAtCursor = itemAtCursor.attribute;
26+
if (attributeAtCursor.referencedBlock) {
27+
block = block.getExportedBlock(attributeAtCursor.referencedBlock);
2728
}
2829

2930
if (!block) {
3031
return [];
3132
}
3233

33-
if (itemAtCursor.parentType === "class") {
34+
if (attributeAtCursor.attributeType === AttributeType.ambiguous) {
35+
let completions: CompletionItem[] = [
36+
{
37+
label: "block:",
38+
kind: CompletionItemKind.Property,
39+
}
40+
];
41+
block.eachBlockExport((name) => {
42+
completions.push({
43+
label: `${name}:`,
44+
kind: CompletionItemKind.Property,
45+
});
46+
});
47+
return completions;
48+
}
49+
50+
if (attributeAtCursor.attributeType === AttributeType.class) {
3451
return block.classes
3552
// TODO: we should look at scope attributes if the user is on the
3653
// root element.
@@ -43,44 +60,44 @@ export async function getHbsCompletions(document: TextDocument, position: Positi
4360
});
4461
}
4562

46-
if (itemAtCursor.parentType === "state") {
63+
if (attributeAtCursor.attributeType === AttributeType.state) {
4764
let completions: CompletionItem[] = [];
4865

49-
if (itemAtCursor.siblingBlocks && itemAtCursor.siblingBlocks.length) {
50-
itemAtCursor.siblingBlocks.forEach(blockSegments => {
51-
if (block && blockSegments.referencedBlock) {
52-
let referencedBlock = block.getExportedBlock(blockSegments.referencedBlock);
53-
54-
if (referencedBlock && blockSegments.className) {
55-
const blockClass = referencedBlock.getClass(blockSegments.className);
66+
// The state might be a partially typed "class" or "scope"
67+
if ("class".startsWith(attributeAtCursor.name)) {
68+
let siblingClass: ClassAttribute | undefined = itemAtCursor.siblingAttributes.find((attr) => attr.referencedBlock === attributeAtCursor.referencedBlock);
69+
if (!siblingClass) { // don't suggest if the class for a block that is already added.
70+
completions.push({
71+
label: "class",
72+
kind: CompletionItemKind.Property,
73+
});
74+
}
75+
}
76+
if ("scope".startsWith(attributeAtCursor.name)) {
77+
if (attributeAtCursor.referencedBlock) { // don't suggest scope for the default block.
78+
completions.push({
79+
label: "scope",
80+
kind: CompletionItemKind.Property,
81+
});
82+
}
83+
}
84+
if (itemAtCursor.siblingAttributes && itemAtCursor.siblingAttributes.length) {
85+
itemAtCursor.siblingAttributes.forEach(classAttribute => {
86+
if (block && classAttribute.referencedBlock === attributeAtCursor.referencedBlock) {
87+
if (classAttribute.name) {
88+
const blockClass = block.getClass(classAttribute.name);
5689
if (blockClass) {
5790
const attributes = blockClass.getAttributes();
5891
completions = completions.concat(attributes.map(
5992
(attr): CompletionItem => {
6093
return {
61-
label: `${attr.namespace}:${attr.name}`,
94+
label: attr.name,
6295
kind: CompletionItemKind.Property,
6396
};
6497
},
6598
));
6699
}
67100
}
68-
} else if (block && blockSegments.className) {
69-
const blockClass = block.getClass(blockSegments.className);
70-
71-
if (blockClass) {
72-
// TODO: this is currently getting all attributes, it should filter
73-
// to state only.
74-
const attributes = blockClass.getAttributes();
75-
completions = completions.concat(attributes.map(
76-
(attr): CompletionItem => {
77-
return {
78-
label: `${attr.namespace}:${attr.name}`,
79-
kind: CompletionItemKind.Property,
80-
};
81-
},
82-
));
83-
}
84101
}
85102
});
86103
}

packages/@css-blocks/language-server/src/util/hbsDefinitionProvider.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { URI } from "vscode-uri";
55

66
import { PathTransformer } from "../pathTransformers/PathTransformer";
77

8-
import { getItemAtCursor } from "./hbsUtils";
8+
import { getItemAtCursor, AttributeType } from "./hbsUtils";
99
import { transformPathsFromUri } from "./pathTransformer";
1010

1111
export async function getHbsDefinition(document: TextDocument, position: Position, blockFactory: BlockFactory, pathTransformer: PathTransformer): Promise<Definition> {
@@ -22,8 +22,8 @@ export async function getHbsDefinition(document: TextDocument, position: Positio
2222
let itemAtCursor = getItemAtCursor(document.getText(), position);
2323

2424
try {
25-
if (itemAtCursor && itemAtCursor.referencedBlock) {
26-
let referencedBlock = block.getExportedBlock(itemAtCursor.referencedBlock);
25+
if (itemAtCursor && itemAtCursor.attribute.referencedBlock) {
26+
let referencedBlock = block.getExportedBlock(itemAtCursor.attribute.referencedBlock);
2727
if (referencedBlock) {
2828
blockUri = URI.file(referencedBlock.identifier).toString();
2929
blockDocumentText = fs.readFileSync(referencedBlock.identifier, {
@@ -43,7 +43,8 @@ export async function getHbsDefinition(document: TextDocument, position: Positio
4343
if (blockDocumentText) {
4444
let lines = blockDocumentText.split(/\r?\n/);
4545
let selectorPositionLine;
46-
let className = (itemAtCursor && itemAtCursor.className) || "";
46+
let attribute = itemAtCursor && itemAtCursor.attribute;
47+
let className = (attribute && attribute.attributeType === AttributeType.class && attribute.name) || "";
4748

4849
for (let i = 0; i < lines.length; i++) {
4950
if (lines[i].includes(className)) {

packages/@css-blocks/language-server/src/util/hbsUtils.ts

+76-37
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
CssBlockError,
55
SourceRange,
66
isNamespaceReserved,
7+
DEFAULT_NAMESPACE,
78
} from "@css-blocks/core";
89
import { AST, preprocess, Walker } from "@glimmer/syntax";
910
import { ElementNode } from "@glimmer/syntax/dist/types/lib/types/nodes";
@@ -27,7 +28,7 @@ function walkClasses(astNode: AST.Node, callback: (namespace: string, classAttr:
2728
console.debug(node);
2829
for (let attrNode of node.attributes) {
2930
let nsAttr = parseNamespacedBlockAttribute(attrNode);
30-
if (isClassAttribute(nsAttr) && attrNode.value.type === "TextNode") {
31+
if (nsAttr && isClassAttribute(nsAttr) && attrNode.value.type === "TextNode") {
3132
callback(nsAttr.ns, attrNode, attrNode.value);
3233
}
3334
}
@@ -170,20 +171,51 @@ export async function validateTemplates(
170171
}, new Map());
171172
}
172173

173-
export const enum SupportedAttributes {
174+
export const enum AttributeType {
174175
state = "state",
175176
class = "class",
176177
scope = "scope",
178+
ambiguous = "ambiguous"
177179
}
178180

179-
interface BlockSegments {
181+
interface BlockAttributeBase {
182+
attributeType: AttributeType;
180183
referencedBlock?: string;
181-
className?: string;
182184
}
183185

184-
interface ItemAtCursor extends BlockSegments {
185-
parentType: SupportedAttributes;
186-
siblingBlocks?: BlockSegments[];
186+
export interface ScopeAttribute extends BlockAttributeBase {
187+
attributeType: AttributeType.scope;
188+
}
189+
190+
export interface ClassAttribute extends BlockAttributeBase {
191+
attributeType: AttributeType.class;
192+
name?: string;
193+
}
194+
195+
export interface StateAttribute extends BlockAttributeBase {
196+
attributeType: AttributeType.state;
197+
name: string;
198+
value?: string;
199+
}
200+
201+
export interface AmbiguousAttribute extends BlockAttributeBase {
202+
attributeType: AttributeType.ambiguous;
203+
referencedBlock?: undefined;
204+
name: string;
205+
}
206+
207+
export type BlockAttribute = ScopeAttribute | ClassAttribute | StateAttribute | AmbiguousAttribute;
208+
209+
interface NamespacedAttr {
210+
ns: string;
211+
name: string;
212+
value?: string;
213+
}
214+
215+
216+
interface ItemAtCursor {
217+
attribute: BlockAttribute;
218+
siblingAttributes: ClassAttribute[];
187219
}
188220

189221
function getParentElement(focusRoot: FocusPath | null): ElementNode | null {
@@ -199,32 +231,29 @@ function getParentElement(focusRoot: FocusPath | null): ElementNode | null {
199231
return null;
200232
}
201233

202-
function buildBlockSegments(attr: NamespacedAttr | null, attrValue: AST.AttrNode["value"]): BlockSegments | null {
234+
function buildClassAttribute(attr: NamespacedAttr | null, attrValue: AST.AttrNode["value"]): ClassAttribute | null {
203235
if (attr === null) return null;
204236
if (attrValue.type === "TextNode") {
205237
if (attr.ns === "block") {
206238
return {
207-
className: attrValue.chars,
239+
attributeType: AttributeType.class,
240+
name: attrValue.chars,
208241
};
209242
} else {
210243
return {
244+
attributeType: AttributeType.class,
211245
referencedBlock: attr.ns,
212-
className: attrValue.chars,
246+
name: attrValue.chars,
213247
};
214248
}
215249
} else {
216250
return null;
217251
}
218252
}
219253

220-
interface NamespacedAttr {
221-
ns: string;
222-
name: string;
223-
}
224-
225254
function parseNamespacedBlockAttribute(attrNode: AST.Node | null | undefined): NamespacedAttr | null {
226255
if (!attrNode || !isAttrNode(attrNode)) return null;
227-
if (/([^:]+):([^:]+)/.test(attrNode.name)) {
256+
if (/([^:]+):([^:]+|$)/.test(attrNode.name)) {
228257
let ns = RegExp.$1;
229258
let name = RegExp.$2;
230259
if (isNamespaceReserved(ns)) {
@@ -239,14 +268,12 @@ function isAttrNode(node: FocusPath | AST.Node | NamespacedAttr | null): node is
239268
return node !== null && ((<AST.Node>node).type) === "AttrNode";
240269
}
241270

242-
function isStateAttribute(attr: NamespacedAttr | null): attr is NamespacedAttr {
243-
if (attr === null) return false;
244-
return attr.name !== SupportedAttributes.class && attr.name !== SupportedAttributes.scope;
271+
function isStateAttribute(attr: NamespacedAttr): boolean {
272+
return attr.name !== AttributeType.class && attr.name !== AttributeType.scope;
245273
}
246274

247-
function isClassAttribute(attr: NamespacedAttr | null): attr is NamespacedAttr {
248-
if (attr === null) return false;
249-
return attr.name === SupportedAttributes.class;
275+
function isClassAttribute(attr: NamespacedAttr): boolean {
276+
return attr.name === AttributeType.class;
250277
}
251278

252279
// TODO: this will be handy when we add support for the scope attribute.
@@ -281,17 +308,25 @@ export function getItemAtCursor(text: string, position: Position): ItemAtCursor
281308

282309
let attr = parseNamespacedBlockAttribute(attrNode);
283310

311+
if (!attr) {
312+
return {
313+
attribute: {
314+
attributeType: AttributeType.ambiguous,
315+
name: attrNode.name,
316+
},
317+
siblingAttributes: [],
318+
};
319+
}
320+
284321
if (isStateAttribute(attr)) {
285-
return getStateAtCursor(focusRoot);
322+
return getStateAtCursor(focusRoot, attr);
286323
}
287324

288325
// TODO: Handle the other types of attribute value nodes
289326
if (isClassAttribute(attr) && data && data.type === "TextNode") {
290-
let blockSegments = buildBlockSegments(attr, data);
291-
if (blockSegments) {
292-
return Object.assign({
293-
parentType: SupportedAttributes.class
294-
}, blockSegments);
327+
let attribute = buildClassAttribute(attr, data);
328+
if (attribute) {
329+
return { attribute, siblingAttributes: []};
295330
} else {
296331
return null;
297332
}
@@ -300,29 +335,33 @@ export function getItemAtCursor(text: string, position: Position): ItemAtCursor
300335
return null;
301336
}
302337

303-
function getStateAtCursor(focusRoot: FocusPath | null) {
338+
function getStateAtCursor(focusRoot: FocusPath | null, attr: NamespacedAttr): ItemAtCursor | null {
304339
let parentElement = getParentElement(focusRoot);
305340

306341
if (!parentElement) {
307342
return null;
308343
}
309-
344+
let attribute: StateAttribute = {
345+
attributeType: AttributeType.state,
346+
referencedBlock: attr.ns === DEFAULT_NAMESPACE ? undefined : attr.ns,
347+
name: attr.name
348+
};
310349
let classAttributes = parentElement.attributes.map(attrNode => {
311350
return [parseNamespacedBlockAttribute(attrNode), attrNode.value] as const;
312351
}).filter(([attr, _attrValue]) => {
313-
return isClassAttribute(attr);
352+
return attr && isClassAttribute(attr);
314353
});
315354

316-
let siblingBlocks = classAttributes.map(([attr, attrValue]) => {
317-
return buildBlockSegments(attr, attrValue);
318-
}).filter((bs): bs is BlockSegments => {
355+
let siblingAttributes = classAttributes.map(([attr, attrValue]) => {
356+
return buildClassAttribute(attr, attrValue);
357+
}).filter((bs): bs is ClassAttribute => {
319358
return bs !== null;
320359
});
321360

322-
if (siblingBlocks.length > 0) {
361+
if (siblingAttributes.length > 0) {
323362
return {
324-
parentType: SupportedAttributes.state,
325-
siblingBlocks,
363+
attribute,
364+
siblingAttributes,
326365
};
327366
} else {
328367
return null;

packages/@css-blocks/vscode/fixtures/app/styles/nav.block.css

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@export subnav from "./subnav.block.css";
2+
13
:scope {
24
color: red;
35
}
@@ -8,4 +10,12 @@
810

911
.foo[is-bar] {
1012
color: blue;
13+
}
14+
15+
.foo[isometric="left"] {
16+
border-left-width: 1px;
17+
}
18+
19+
.foo[isometric="right"] {
20+
border-right-width: 1px;
1121
}

0 commit comments

Comments
 (0)