Skip to content

Commit

Permalink
feat(vscode-pv-handlebars-language-server): add goto definition suppo…
Browse files Browse the repository at this point in the history
…rt for ui prop/selectors in TS
  • Loading branch information
mbehzad committed Jun 21, 2024
1 parent e176ca3 commit 59e6b7e
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { getCustomElementsUIAndEvents } from "./customElementDefinitionProvider"
import { getFilePath } from "./helpers";
import rgx from "./rgx";


/**
* creates an object that can be consumed by onCodeLens request handler (see vscode's `CodeLens` interface for more info)
*
Expand Down Expand Up @@ -54,16 +53,26 @@ export async function codelensProvider(textDocument: TextDocument) {
if (!selectors) return null;

const content = textDocument.getText();
const regex = new RegExp(rgx.hbs.classNamesAndTags(), "g");
let matches;
// supporting ui selector being a css class, or a tag name for now
const matches = [
...content.matchAll(new RegExp(rgx.hbs.tags(), "g")),
...content.matchAll(new RegExp(rgx.hbs.classNames(), "g")),
];

while ((matches = regex.exec(content)) !== null) {
for (const match of matches) {
for (const [selector, item] of Object.entries(selectors)) {
const classMatch =
selector.startsWith(".") && matches.groups!.className?.split(" ").includes(selector.replace(/^./, ""));
const tagMatch = matches.groups!.tagName === selector;
selector.startsWith(".") &&
match
// remove handlebar expressions
.groups!.className?.replaceAll(/{{.*?}}/g, "")
// split each css class
.split(" ")
// check if one is the same as the ui selector
.includes(selector.replace(/^./, ""));
const tagMatch = match.groups!.tagName === selector;
if (classMatch || tagMatch) {
const line = content.substring(0, matches.index).split("\n").length - 1;
const line = content.substring(0, match.index).split("\n").length - 1;

if (item.ui) {
codeLenses.push(
Expand Down
58 changes: 41 additions & 17 deletions packages/vscode-pv-handlebars-language-server/server/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,29 +311,33 @@ export async function getCssClasses(filePath: string) {

for (const hbsFile of templates) {
const fileContent = hbsFile.fileContent;
const regex = new RegExp(rgx.hbs.classNamesAndTags(), "g");
let matches;
while ((matches = regex.exec(fileContent)) !== null) {
if (!matches.groups!.className) continue;

const contentBefore = fileContent.substring(0, matches.index);
const line = contentBefore.split("\n").length - 1;
const character = matches.index - contentBefore.lastIndexOf("\n");
let className = matches.groups!.className;
const matches = Array.from(fileContent.matchAll(new RegExp(rgx.hbs.classNames(), "g")));
for (const match of matches) {
let className = match.groups!.className;
className = className.replace(/{{.*?}}/g, "");

const classes = className
.split(" ")
.map(c => c.trim())
.filter(c => c !== "");

cssClasses.push(
...classes.map(clss => ({
className: clss,
location: {
filePath,
line,
character,
},
})),
...classes.map(clss => {
const start = getPositionAt(fileContent, match!.index + match![0].indexOf(clss));
return {
className: clss,
location: {
filePath: hbsFile.filePath,
range: {
start: start,
end: {
line: start.line,
character: start.character + clss.length,
},
},
},
};
}),
);
}
}
Expand All @@ -358,6 +362,7 @@ export async function getNestedTemplates(hbsTemplate: string) {
}
} else if (!visited.includes(filePaths)) {
visited.push(filePaths);

const fileContent = await getFileContent(filePaths);
results.push({ filePath: filePaths, fileContent });

Expand All @@ -381,3 +386,22 @@ export async function getNestedTemplates(hbsTemplate: string) {

return searchRecursive(hbsTemplate);
}

/**
* Converts a zero-based offset to a position (line, character).
*
* @param {text} text
* @param {number} offset
* @returns {line: number, character: number}
*/
export function getPositionAt(text: string, offset: number) {
const textBefore = text.substring(0, offset);
const lines = textBefore.split("\n");
const line = lines.length - 1;
const character = lines.at(-1)!.length;

return {
line,
character,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export default {
endsWithEventListenerDecoratorTarget: () => /@eventListener\({[^}]*target:\s*"[^"]*$/,
},
hbs: {
// `<some-tag class="className {{#if foo}}className2{{/if}}"`
classNamesAndTags: () => /<(?<tagName>[a-zA-Z0-9_-]+)[^>]*?(class="(?<className>[^"]*))?"/,
// `<some-tag`
tags: () => /<(?<tagName>[a-zA-Z0-9_-]+)/,
// `<some-tag ... class="className {{...}}className2"`
classNames: () => /<(?<tagName>[a-zA-Z0-9_-]+)[^>]*?class="(?<className>[^"]*)"/,
// {{#> some-partial
partials: () => /{{#?>\s*(?<partial>[-_a-zA-Z0-9]+)/g,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { hoverProvider } from "./hoverProvider";
import { getFilePath, isHandlebarsFile, isTypescriptFile } from "./helpers";
import { codelensProvider } from "./codelensProvider";
import { tsCompletionProvider } from "./tsCompletionProvider";
import { tsDefinitionProvider } from "./tsDefinitionProvider";

// Create a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
Expand Down Expand Up @@ -115,6 +116,7 @@ connection.onDefinition(({ textDocument, position }) => {
if (document) {
const filePath = getFilePath(document);
if (isHandlebarsFile(filePath)) return definitionProvider(document, position, filePath);
else if (isTypescriptFile(filePath)) return tsDefinitionProvider(document, position, filePath);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Location, Position } from "vscode-languageserver/node";
import { TextDocument } from "vscode-languageserver-textdocument";
import { URI } from "vscode-uri";
import { isPVArchetype, getCurrentSymbolsName, getCssClasses, getPositionAt } from "./helpers";
import rgx from "./rgx";

export async function tsDefinitionProvider(
document: TextDocument,
position: Position,
filePath: string,
): Promise<Location | Location[] | null> {
// simple check if it is a component ala p!v archetype
if (!isPVArchetype(filePath)) return null;

const offset = document.offsetAt(position);
const originalText = document.getText();
const textBefore = originalText.slice(0, offset);

const symbolName = getCurrentSymbolsName(document, position);
// `.some-selector` and not `uiProp`
const isCssClassSelector = /\.[a-zA-Z_-]+$/.test(textBefore);

// `@uiElement("` and `@uiElements("` or `// @eventListener({ ... target: ".class`
if (
rgx.ts.endsWithUiDecoratorSelector().test(textBefore) ||
(rgx.ts.endsWithEventListenerDecoratorTarget().test(textBefore) && isCssClassSelector)
) {
const cssClasses = await getCssClasses(filePath);
return cssClasses
.filter(cssClass => cssClass.className === symbolName)
.map(cssClass => Location.create(URI.file(cssClass.location.filePath).toString(), cssClass.location.range));
}
// `@uiEvent("uiProp` or `@eventListener({ ... target: "uiProp`
else if (
rgx.ts.endsWithEventDecoratorElementName().test(textBefore) ||
(rgx.ts.endsWithEventListenerDecoratorTarget().test(textBefore) && !isCssClassSelector)
) {
const uiMatch = originalText.match(new RegExp(`@uiElements?\\(.+?\\)\\s*${symbolName}`));
if (uiMatch) {
const deco = uiMatch[0].match(/@uiElements?\(.+?\)\s*/)![0];
const start = getPositionAt(originalText, uiMatch.index! + deco.length);
return Location.create(document.uri, {
start,
end: {
line: start.line,
character: start.character + symbolName.length,
},
});
}
}
return null;
}

0 comments on commit 59e6b7e

Please sign in to comment.