Skip to content

Commit c774319

Browse files
committed
Implement definition and hover providers for javascript files
1 parent 93e73f4 commit c774319

File tree

8 files changed

+161
-15
lines changed

8 files changed

+161
-15
lines changed

src/common/css-class-definition.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
import * as vscode from "vscode";
44

55
class CssClassDefinition {
6-
public constructor(public className: string, public location?: vscode.Location) { }
6+
public constructor(public className: string) { }
7+
8+
/** Documentation comments written in front of the definition. */
9+
comments?: string[];
10+
11+
location?: vscode.Location;
712
}
813

914
export default CssClassDefinition;

src/extension.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import "source-map-support/register";
44
import * as VError from "verror";
55
import {
66
commands, CompletionItem, CompletionItemKind, Disposable,
7-
ExtensionContext, languages, Position, Range, TextDocument, Uri, window,
7+
ExtensionContext, Hover, languages, Location, Position, Range, TextDocument, Uri, window,
88
workspace,
99
} from "vscode";
1010
import CssClassDefinition from "./common/css-class-definition";
@@ -148,6 +148,11 @@ const registerCompletionProvider = (
148148

149149
completionItem.filterText = completionClassName;
150150
completionItem.insertText = completionClassName;
151+
completionItem.range = document.getWordRangeAtPosition(position, /[-\w,@\\:\[\]]+/);
152+
153+
if (definition.comments && definition.comments.length !== 0) {
154+
completionItem.detail = definition.comments![0].split(/\r?\n/, 2)[0];
155+
}
151156

152157
return completionItem;
153158
});
@@ -165,6 +170,72 @@ const registerCompletionProvider = (
165170
},
166171
}, ...completionTriggerChars);
167172

173+
const registerDefinitionProvider = (languageSelector: string, classMatchRegex: RegExp) => languages.registerDefinitionProvider(languageSelector, {
174+
provideDefinition(document, position, _token) {
175+
// Check if the cursor is on a class attribute and retrieve all the css rules in this class attribute
176+
{
177+
const start: Position = new Position(position.line, 0);
178+
const range: Range = new Range(start, position);
179+
const text: string = document.getText(range);
180+
181+
const rawClasses: RegExpMatchArray | null = text.match(classMatchRegex);
182+
if (!rawClasses || rawClasses.length === 1) {
183+
return;
184+
}
185+
}
186+
187+
const range: Range | undefined = document.getWordRangeAtPosition(position, /[-\w,@\\:\[\]]+/);
188+
if (range == null) {
189+
return;
190+
}
191+
192+
const word: string = document.getText(range);
193+
194+
const definition = uniqueDefinitions.find((definition) => {
195+
return definition.className === word;
196+
});
197+
if (definition == null || !definition.location) {
198+
return;
199+
}
200+
201+
return definition.location as Location;
202+
},
203+
})
204+
205+
const registerHoverProvider = (languageSelector: string, classMatchRegex: RegExp) => languages.registerHoverProvider(languageSelector, {
206+
provideHover(document, position, _token) {
207+
{
208+
const start: Position = new Position(position.line, 0);
209+
const range: Range = new Range(start, position);
210+
const text: string = document.getText(range);
211+
212+
const rawClasses: RegExpMatchArray | null = text.match(classMatchRegex);
213+
if (!rawClasses || rawClasses.length === 1) {
214+
return;
215+
}
216+
}
217+
218+
const range: Range | undefined = document.getWordRangeAtPosition(position, /[-\w,@\\:\[\]]+/);
219+
if (range == null) {
220+
return;
221+
}
222+
223+
const word: string = document.getText(range);
224+
225+
// Creates a collection of CompletionItem based on the classes already cached
226+
const definition = uniqueDefinitions.find((definition) => {
227+
return definition.className === word
228+
});
229+
if (definition == null) {
230+
return;
231+
}
232+
233+
if (definition.comments != null) {
234+
return new Hover(`**.\`${word}\`**\n${definition.comments.join("\n\n")}`, range)
235+
}
236+
},
237+
})
238+
168239
const registerHTMLProviders = (disposables: Disposable[]) =>
169240
workspace.getConfiguration()
170241
?.get<string[]>(Configuration.HTMLLanguages)
@@ -186,8 +257,10 @@ const registerJavaScriptProviders = (disposables: Disposable[]) =>
186257
workspace.getConfiguration()
187258
.get<string[]>(Configuration.JavaScriptLanguages)
188259
?.forEach((extension) => {
189-
disposables.push(registerCompletionProvider(extension, /className=(?:{?"|{?')([\w-@:\/ ]*$)/));
260+
disposables.push(registerCompletionProvider(extension, /className=(?:{?"|{?'|{?`)([\w-@:\/ ]*$)/));
190261
disposables.push(registerCompletionProvider(extension, /class=(?:{?"|{?')([\w-@:\/ ]*$)/));
262+
disposables.push(registerDefinitionProvider(extension, /class(?:Name)?=["|']([\w- ]*$)/));
263+
disposables.push(registerHoverProvider(extension, /class(?:Name)?=["|']([\w- ]*$)/));
191264
});
192265

193266
function registerEmmetProviders(disposables: Disposable[]) {

src/parse-engine-gateway.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ async function createSimpleTextDocument(uri: vscode.Uri): Promise<ISimpleTextDoc
2525
getText(): string {
2626
return text;
2727
},
28+
uri,
2829
};
2930
return simpleDocument;
3031
}
@@ -33,7 +34,7 @@ class ParseEngineGateway {
3334
public static async callParser(uri: vscode.Uri): Promise<CssClassDefinition[]> {
3435
const textDocument = await createSimpleTextDocument(uri);
3536
const parseEngine: IParseEngine = ParseEngineRegistry.getParseEngine(textDocument.languageId);
36-
const cssClassDefinitions: CssClassDefinition[] = await parseEngine.parse(textDocument);
37+
const cssClassDefinitions: CssClassDefinition[] = await parseEngine.parse(textDocument, uri);
3738
return cssClassDefinitions;
3839
}
3940
}
Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,94 @@
11
import * as css from "css";
2+
import * as vscode from "vscode";
23
import CssClassDefinition from "../../common/css-class-definition";
34

45
export default class CssClassExtractor {
56
/**
67
* @description Extracts class names from CSS AST
78
*/
8-
public static extract(ast: css.Stylesheet): CssClassDefinition[] {
9+
public static extract(ast: css.Stylesheet, uri: vscode.Uri | undefined): CssClassDefinition[] {
910
const classNameRegex = /[.](([\w-]|\\[@:/])+)/g;
1011

1112
const definitions: CssClassDefinition[] = [];
1213

1314
// go through each of the selectors of the current rule
14-
const addRule = (rule: css.Rule) => {
15+
const addRule = (rule: css.Rule, comments: string[] | undefined) => {
1516
rule.selectors?.forEach((selector: string) => {
1617
let item: RegExpExecArray | null = classNameRegex.exec(selector);
1718
while (item) {
18-
definitions.push(new CssClassDefinition(item[1].replace("\\", "")));
19+
const definition = new CssClassDefinition(item[1].replace("\\", ""));
20+
definition.comments = comments;
21+
definition.location = toLocation(rule, uri);
22+
definitions.push(definition);
23+
1924
item = classNameRegex.exec(selector);
2025
}
2126
});
2227
};
2328

2429
// go through each of the rules or media query...
25-
ast.stylesheet?.rules.forEach((rule: css.Rule & css.Media) => {
30+
ast.stylesheet?.rules.forEach((rule: css.Rule & css.Media, index) => {
2631
// ...of type rule
2732
if (rule.type === "rule") {
28-
addRule(rule);
33+
addRule(rule, collectComments(ast.stylesheet!.rules, index));
2934
}
3035
// of type media queries
3136
if (rule.type === "media") {
3237
// go through rules inside media queries
33-
rule.rules?.forEach((rule: css.Rule) => addRule(rule));
38+
rule.rules?.forEach((r: css.Rule, i) => addRule(r, collectComments(rule.rules!, i)));
3439
}
3540
});
3641
return definitions;
3742
}
3843
}
44+
45+
function collectComments(rules: (css.Rule | css.Comment | css.AtRule)[], index: number): string[] | undefined {
46+
if (!rules || index === 0) {
47+
return undefined;
48+
}
49+
50+
const comments = [];
51+
for (let j = index - 1; j >= 0; j--) {
52+
const node = rules[j] as { comment?: string };
53+
if (!node.comment) {
54+
break;
55+
}
56+
57+
// Only if it looks like `/** ... */`.
58+
if (node.comment.startsWith("*")) {
59+
comments.push(node.comment.slice(1).trim());
60+
}
61+
}
62+
if (comments.length === 0) {
63+
return undefined;
64+
}
65+
66+
comments.reverse();
67+
return comments;
68+
}
69+
70+
const toLocation = (node: css.Node, uri: vscode.Uri | undefined) => {
71+
if (!uri || !node.position) {
72+
return undefined;
73+
}
74+
75+
const start = node.position.start && toPosition(node.position.start);
76+
if (!start) {
77+
return undefined;
78+
}
79+
80+
const end = node.position.end && toPosition(node.position.end);
81+
if (!end) {
82+
return undefined;
83+
}
84+
85+
return new vscode.Location(uri, new vscode.Range(start, end));
86+
}
87+
88+
const toPosition = (node: css.Position) => {
89+
if (node.line == null || node.column == null) {
90+
return undefined;
91+
}
92+
93+
return new vscode.Position(node.line - 1, node.column - 1);
94+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import * as vscode from "vscode";
12
import CssClassDefinition from "./../../common/css-class-definition";
23
import ISimpleTextDocument from "./simple-text-document";
34

45
interface IParseEngine {
56
languageId: string;
67
extension: string;
7-
parse(textDocument: ISimpleTextDocument): Promise<CssClassDefinition[]>;
8+
parse(textDocument: ISimpleTextDocument, uri: vscode.Uri): Promise<CssClassDefinition[]>;
89
}
910

1011
export default IParseEngine;

src/parse-engines/common/simple-text-document.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
interface ISimpleTextDocument {
55
languageId: string;
66
getText(): string;
7+
8+
/** URI. The value must be of `vscode.Uri | undefined`. */
9+
uri?: unknown;
710
}
811

912
export default ISimpleTextDocument;

src/parse-engines/types/css-parse-engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class CssParseEngine implements IParseEngine {
1212
const code: string = textDocument.getText();
1313
const codeAst: css.Stylesheet = css.parse(code);
1414

15-
return CssClassExtractor.extract(codeAst);
15+
return CssClassExtractor.extract(codeAst, textDocument.uri as any);
1616
}
1717
}
1818

src/parse-engines/types/html-parse-engine.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Bluebird from "bluebird";
22
import * as css from "css";
33
import * as html from "htmlparser2";
44
import * as request from "request-promise";
5+
import * as vscode from "vscode";
56
import CssClassDefinition from "../../common/css-class-definition";
67
import CssClassExtractor from "../common/css-class-extractor";
78
import IParseEngine from "../common/parse-engine";
@@ -11,7 +12,7 @@ class HtmlParseEngine implements IParseEngine {
1112
public languageId = "html";
1213
public extension = "html";
1314

14-
public async parse(textDocument: ISimpleTextDocument): Promise<CssClassDefinition[]> {
15+
public async parse(textDocument: ISimpleTextDocument, uri: vscode.Uri | undefined): Promise<CssClassDefinition[]> {
1516
const definitions: CssClassDefinition[] = [];
1617
const urls: string[] = [];
1718
let tag: string;
@@ -41,7 +42,7 @@ class HtmlParseEngine implements IParseEngine {
4142
},
4243
ontext: (text: string) => {
4344
if (tag === "style") {
44-
definitions.push(...CssClassExtractor.extract(css.parse(text)));
45+
definitions.push(...CssClassExtractor.extract(css.parse(text), uri));
4546
}
4647
},
4748
});
@@ -51,7 +52,13 @@ class HtmlParseEngine implements IParseEngine {
5152

5253
await Bluebird.map(urls, async (url) => {
5354
const content = await request.get(url);
54-
definitions.push(...CssClassExtractor.extract(css.parse(content)));
55+
let uri: vscode.Uri | undefined;
56+
try {
57+
uri = vscode.Uri.parse(url);
58+
} catch (err) {
59+
// Tolerable.
60+
}
61+
definitions.push(...CssClassExtractor.extract(css.parse(content), uri));
5562
}, { concurrency: 10 });
5663

5764
return definitions;

0 commit comments

Comments
 (0)