mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] owl-vision: Autocomplete and added missing owl directives
This commit adds basic autocomplete in xml files. This includes autocompletion for elements, components, props, attributes, and javascript expressions. It also: - Adds "Go To Definition" support for props and javascript expressions in xml - Support for the following directives: t-att, t-model, t-tag, t-debug, t-log - Fixes t-else syntax highlight to be non-dynamic as the attribute value should be empty
This commit is contained in:
committed by
Géry Debongnie
parent
2eb151c92d
commit
55c48b2b12
@@ -4,6 +4,28 @@ All notable changes to the "owl-vision" extension will be documented in this fil
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
|
||||||
|
## [0.1.0] - 2024-11-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Basic autocomplete in xml files. This includes autocompletion for elements, components,
|
||||||
|
props, attributes, and javascript expressions.
|
||||||
|
|
||||||
|
The current implementation, while relatively simple, has a couple of drawbacks:
|
||||||
|
- Javascript imports are not resolved by the xml autocomplete, this means that it does not
|
||||||
|
understand the types of imported functions or objects. That said, I've added custom support
|
||||||
|
for frequently used Owl imports, namely `useState` and `useRef`. You can add more in
|
||||||
|
the settings if needed.
|
||||||
|
- The autocomplete is limited to templates directly linked to components, sub-templates
|
||||||
|
used via t-call will not get autocompletion as no component/context can be bound to them.
|
||||||
|
|
||||||
|
- "Go To Definition" support for props and javascript expressions in xml
|
||||||
|
- Support for the following directives: t-att, t-model, t-tag, t-debug, t-log
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Changed t-else syntax highlight from dynamic to static attribute
|
||||||
|
|
||||||
## [0.0.2] - 2023-2-11
|
## [0.0.2] - 2023-2-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+5557
-5527
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"description": "Owl framework extension that highlights templates and ease navigation between components and templates.",
|
"description": "Owl framework extension that highlights templates and ease navigation between components and templates.",
|
||||||
"publisher": "Odoo",
|
"publisher": "Odoo",
|
||||||
"license": "LGPL-3.0-only",
|
"license": "LGPL-3.0-only",
|
||||||
"version": "0.0.2",
|
"version": "0.1.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/odoo/owl/tree/master/tools/owl-vision"
|
"url": "https://github.com/odoo/owl/tree/master/tools/owl-vision"
|
||||||
@@ -63,6 +63,13 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "**/node_modules/**,**/lib/**,**/tests/**",
|
"default": "**/node_modules/**,**/lib/**,**/tests/**",
|
||||||
"description": "Files to exclude in search"
|
"description": "Files to exclude in search"
|
||||||
|
},
|
||||||
|
"owl-vision.autocomplete-mocks": {
|
||||||
|
"order": 2,
|
||||||
|
"type": "string",
|
||||||
|
"editPresentation": "multilineText",
|
||||||
|
"default": "/**\n* @template T\n* @param {T} obj\n* @returns {T}\n*/\nfunction useState(obj) {}\n\n/**\n* @typedef {Object} Ref\n* @property {HTMLElement} el\n*/\n/**\n* @returns {Ref}\n*/\nfunction useRef(name) {}",
|
||||||
|
"description": "Mocks for functions or object that are imported but not resolved by the autcomplete. Add docstring comments for them to work properly."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -128,6 +135,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "20.2.5",
|
"@types/node": "20.2.5",
|
||||||
"@types/vscode": "^1.73.0",
|
"@types/vscode": "^1.73.0",
|
||||||
|
"@types/xmldoc": "^1.1.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||||
"@typescript-eslint/parser": "^5.59.8",
|
"@typescript-eslint/parser": "^5.59.8",
|
||||||
"@vscode/test-electron": "^2.3.2",
|
"@vscode/test-electron": "^2.3.2",
|
||||||
@@ -135,5 +143,8 @@
|
|||||||
"esbuild": "^0.19.5",
|
"esbuild": "^0.19.5",
|
||||||
"eslint": "^8.41.0",
|
"eslint": "^8.41.0",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"xmldoc": "^1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ export const propsAttributes = createAttributePatterns("props-attributes", {
|
|||||||
export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dynamic", {
|
export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dynamic", {
|
||||||
match: [
|
match: [
|
||||||
"t-if",
|
"t-if",
|
||||||
"t-else",
|
|
||||||
"t-elif",
|
"t-elif",
|
||||||
"t-foreach",
|
"t-foreach",
|
||||||
"t-as",
|
"t-as",
|
||||||
@@ -75,6 +74,10 @@ export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dyna
|
|||||||
"t-value",
|
"t-value",
|
||||||
"t-portal",
|
"t-portal",
|
||||||
"t-slot-scope",
|
"t-slot-scope",
|
||||||
|
"t-att",
|
||||||
|
"t-tag",
|
||||||
|
"t-log",
|
||||||
|
"t-model",
|
||||||
"t-att-[a-z_:.-]+",
|
"t-att-[a-z_:.-]+",
|
||||||
"t-on-[a-z_:.-]+"
|
"t-on-[a-z_:.-]+"
|
||||||
].join("|"),
|
].join("|"),
|
||||||
@@ -86,12 +89,13 @@ export const owlAttributesDynamic = createAttributePatterns("owl-attributes-dyna
|
|||||||
export const owlAttributesStatic = createAttributePatterns("owl-attributes-static", {
|
export const owlAttributesStatic = createAttributePatterns("owl-attributes-static", {
|
||||||
match: [
|
match: [
|
||||||
"t-name",
|
"t-name",
|
||||||
|
"t-else",
|
||||||
"t-ref",
|
"t-ref",
|
||||||
"t-set-slot",
|
"t-set-slot",
|
||||||
"t-model",
|
|
||||||
"t-inherit",
|
"t-inherit",
|
||||||
"t-inherit-mode",
|
"t-inherit-mode",
|
||||||
"t-translation"
|
"t-translation",
|
||||||
|
"t-debug",
|
||||||
].join("|"),
|
].join("|"),
|
||||||
attributeName: "owl.attribute owl.attribute.static",
|
attributeName: "owl.attribute owl.attribute.static",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import * as vscode from 'vscode';
|
|
||||||
import { getSelectedText, showStatusMessage, hideStatusMessage } from './utils';
|
|
||||||
import { Search } from './search';
|
|
||||||
|
|
||||||
export class ComponentDefinitionProvider implements vscode.DefinitionProvider {
|
|
||||||
|
|
||||||
search: Search;
|
|
||||||
|
|
||||||
constructor(search: Search) {
|
|
||||||
this.search = search;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface implementation to provide definition when ctrl+click on Component
|
|
||||||
* tag in template.
|
|
||||||
*/
|
|
||||||
async provideDefinition(document: vscode.TextDocument, position: vscode.Position) {
|
|
||||||
const currentWord = getSelectedText(/<\/?[A-Z][a-zA-Z]+/, document, position);
|
|
||||||
if (!currentWord) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const componentName = currentWord.replace(/[\/<]/g, "").trim();
|
|
||||||
|
|
||||||
showStatusMessage(`Searching for component "${componentName}"`);
|
|
||||||
const result = await this.search.findComponent(componentName);
|
|
||||||
hideStatusMessage();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { Search } from './search';
|
import { Search } from './search';
|
||||||
import { ComponentDefinitionProvider } from './definiton_providers';
|
|
||||||
import { OpenDirection } from './utils';
|
import { OpenDirection } from './utils';
|
||||||
|
import { OwlLanguageFeaturesProvider } from './language_features/language_features_provider';
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext) {
|
export async function activate(context: vscode.ExtensionContext) {
|
||||||
const search = new Search();
|
const search = new Search();
|
||||||
|
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch', () => search.switch()));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch', () => search.switch()));
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-besides', () => search.switch(OpenDirection.Besides)));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-besides', () => search.switch(OpenDirection.Besides)));
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-below', () => search.switch(OpenDirection.Below)));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.switch-below', () => search.switch(OpenDirection.Below)));
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-component', () => search.findComponentCommand()));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-component', () => search.findComponentCommand()));
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-template', () => search.findTemplateCommand()));
|
context.subscriptions.push(vscode.commands.registerCommand('owl-vision.find-template', () => search.findTemplateCommand()));
|
||||||
|
|
||||||
const componentDefProvider = new ComponentDefinitionProvider(search);
|
const languageFeaturesProvider = new OwlLanguageFeaturesProvider(search);
|
||||||
context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'xml' }, componentDefProvider));
|
context.subscriptions.push(vscode.languages.registerCompletionItemProvider({ language: 'xml', scheme: 'file' }, languageFeaturesProvider, '.', '<'));
|
||||||
context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'javascript' }, componentDefProvider));
|
context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'xml', scheme: 'file' }, languageFeaturesProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate() { }
|
export function deactivate() { }
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
import { CompletionItemKind } from "vscode"
|
||||||
|
|
||||||
|
export const owlComponentAttributes = [
|
||||||
|
"t-if",
|
||||||
|
"t-else",
|
||||||
|
"t-elif",
|
||||||
|
"t-foreach",
|
||||||
|
"t-as",
|
||||||
|
"t-key",
|
||||||
|
"t-esc",
|
||||||
|
"t-out",
|
||||||
|
"t-props",
|
||||||
|
"t-set",
|
||||||
|
"t-value",
|
||||||
|
"t-portal",
|
||||||
|
"t-slot-scope",
|
||||||
|
"t-log",
|
||||||
|
].map(label => ({
|
||||||
|
label: label,
|
||||||
|
insertText: label + '=""',
|
||||||
|
kind: CompletionItemKind.Property,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const owlElementAttributes = [
|
||||||
|
"t-component",
|
||||||
|
"t-att",
|
||||||
|
"t-tag",
|
||||||
|
"t-model",
|
||||||
|
].map(label => ({
|
||||||
|
label: label,
|
||||||
|
insertText: label + '=""',
|
||||||
|
kind: CompletionItemKind.Property,
|
||||||
|
}));
|
||||||
|
|
||||||
|
owlElementAttributes.push(...owlComponentAttributes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
To generate this list, run the following snippet on https://developer.mozilla.org/fr/docs/Web/Events
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const events = [...document.querySelectorAll(".section-content li a")]
|
||||||
|
.map(e => e.childNodes[0])
|
||||||
|
.filter(n => n.nodeType === 3 && n.textContent.toLowerCase() == n.textContent)
|
||||||
|
.map(n => "t-on-" + n.textContent.trim());
|
||||||
|
return JSON.stringify([...new Set(events)], null, 2);
|
||||||
|
}())
|
||||||
|
*/
|
||||||
|
export const events = [
|
||||||
|
"t-on-abort",
|
||||||
|
"t-on-ended",
|
||||||
|
"t-on-addtrack",
|
||||||
|
"t-on-change",
|
||||||
|
"t-on-removetrack",
|
||||||
|
"t-on-messageerror",
|
||||||
|
"t-on-message",
|
||||||
|
"t-on-animationcancel",
|
||||||
|
"t-on-animationend",
|
||||||
|
"t-on-animationiteration",
|
||||||
|
"t-on-animationstart",
|
||||||
|
"t-on-copy",
|
||||||
|
"t-on-cut",
|
||||||
|
"t-on-dragend",
|
||||||
|
"t-on-dragenter",
|
||||||
|
"t-on-dragleave",
|
||||||
|
"t-on-dragover",
|
||||||
|
"t-on-dragstart",
|
||||||
|
"t-on-drag",
|
||||||
|
"t-on-drop",
|
||||||
|
"t-on-fullscreenchange",
|
||||||
|
"t-on-fullscreenerror",
|
||||||
|
"t-on-gotpointercapture",
|
||||||
|
"t-on-keydown",
|
||||||
|
"t-on-keypress",
|
||||||
|
"t-on-keyup",
|
||||||
|
"t-on-lostpointercapture",
|
||||||
|
"t-on-paste",
|
||||||
|
"t-on-pointercancel",
|
||||||
|
"t-on-pointerdown",
|
||||||
|
"t-on-pointerenter",
|
||||||
|
"t-on-pointerleave",
|
||||||
|
"t-on-pointerlockchange",
|
||||||
|
"t-on-pointerlockerror",
|
||||||
|
"t-on-pointermove",
|
||||||
|
"t-on-pointerout",
|
||||||
|
"t-on-pointerover",
|
||||||
|
"t-on-pointerup",
|
||||||
|
"t-on-readystatechange",
|
||||||
|
"t-on-scroll",
|
||||||
|
"t-on-selectionchange",
|
||||||
|
"t-on-selectstart",
|
||||||
|
"t-on-touchcancel",
|
||||||
|
"t-on-touchend",
|
||||||
|
"t-on-touchmove",
|
||||||
|
"t-on-touchstart",
|
||||||
|
"t-on-transitioncancel",
|
||||||
|
"t-on-transitionend",
|
||||||
|
"t-on-transitionrun",
|
||||||
|
"t-on-transitionstart",
|
||||||
|
"t-on-visibilitychange",
|
||||||
|
"t-on-wheel",
|
||||||
|
"t-on-afterscriptexecute",
|
||||||
|
"t-on-auxclick",
|
||||||
|
"t-on-beforescriptexecute",
|
||||||
|
"t-on-blur",
|
||||||
|
"t-on-click",
|
||||||
|
"t-on-compositionend",
|
||||||
|
"t-on-compositionstart",
|
||||||
|
"t-on-compositionupdate",
|
||||||
|
"t-on-contextmenu",
|
||||||
|
"t-on-dblclick",
|
||||||
|
"t-on-error",
|
||||||
|
"t-on-focusin",
|
||||||
|
"t-on-focusout",
|
||||||
|
"t-on-focus",
|
||||||
|
"t-on-gesturechange",
|
||||||
|
"t-on-gestureend",
|
||||||
|
"t-on-gesturestart",
|
||||||
|
"t-on-mousedown",
|
||||||
|
"t-on-mouseenter",
|
||||||
|
"t-on-mouseleave",
|
||||||
|
"t-on-mousemove",
|
||||||
|
"t-on-mouseout",
|
||||||
|
"t-on-mouseover",
|
||||||
|
"t-on-mouseup",
|
||||||
|
"t-on-mousewheel",
|
||||||
|
"t-on-overflow",
|
||||||
|
"t-on-select",
|
||||||
|
"t-on-show",
|
||||||
|
"t-on-underflow",
|
||||||
|
"t-on-webkitmouseforcechanged",
|
||||||
|
"t-on-webkitmouseforcedown",
|
||||||
|
"t-on-webkitmouseforceup",
|
||||||
|
"t-on-webkitmouseforcewillbegin",
|
||||||
|
"t-on-open",
|
||||||
|
"t-on-loadend",
|
||||||
|
"t-on-loadstart",
|
||||||
|
"t-on-load",
|
||||||
|
"t-on-progress",
|
||||||
|
"t-on-webglcontextcreationerror",
|
||||||
|
"t-on-webglcontextlost",
|
||||||
|
"t-on-webglcontextrestored",
|
||||||
|
"t-on-toggle",
|
||||||
|
"t-on-cancel",
|
||||||
|
"t-on-close",
|
||||||
|
"t-on-beforeinput",
|
||||||
|
"t-on-input",
|
||||||
|
"t-on-formdata",
|
||||||
|
"t-on-reset",
|
||||||
|
"t-on-submit",
|
||||||
|
"t-on-invalid",
|
||||||
|
"t-on-search",
|
||||||
|
"t-on-canplaythrough",
|
||||||
|
"t-on-canplay",
|
||||||
|
"t-on-durationchange",
|
||||||
|
"t-on-emptied",
|
||||||
|
"t-on-loadeddata",
|
||||||
|
"t-on-loadedmetadata",
|
||||||
|
"t-on-pause",
|
||||||
|
"t-on-playing",
|
||||||
|
"t-on-play",
|
||||||
|
"t-on-ratechange",
|
||||||
|
"t-on-seeked",
|
||||||
|
"t-on-seeking",
|
||||||
|
"t-on-stalled",
|
||||||
|
"t-on-suspend",
|
||||||
|
"t-on-timeupdate",
|
||||||
|
"t-on-volumechange",
|
||||||
|
"t-on-waiting",
|
||||||
|
"t-on-slotchange",
|
||||||
|
"t-on-cuechange",
|
||||||
|
"t-on-enterpictureinpicture",
|
||||||
|
"t-on-leavepictureinpicture",
|
||||||
|
"t-on-versionchange",
|
||||||
|
"t-on-blocked",
|
||||||
|
"t-on-upgradeneeded",
|
||||||
|
"t-on-success",
|
||||||
|
"t-on-complete",
|
||||||
|
"t-on-devicechange",
|
||||||
|
"t-on-mute",
|
||||||
|
"t-on-unmute",
|
||||||
|
"t-on-merchantvalidation",
|
||||||
|
"t-on-paymentmethodchange",
|
||||||
|
"t-on-shippingaddresschange",
|
||||||
|
"t-on-shippingoptionchange",
|
||||||
|
"t-on-payerdetailchange",
|
||||||
|
"t-on-resourcetimingbufferfull",
|
||||||
|
"t-on-resize",
|
||||||
|
"t-on-bufferedamountlow",
|
||||||
|
"t-on-closing",
|
||||||
|
"t-on-tonechange",
|
||||||
|
"t-on-gatheringstatechange",
|
||||||
|
"t-on-selectedcandidatepairchange",
|
||||||
|
"t-on-statechange",
|
||||||
|
"t-on-addstream",
|
||||||
|
"t-on-connectionstatechange",
|
||||||
|
"t-on-datachannel",
|
||||||
|
"t-on-icecandidateerror",
|
||||||
|
"t-on-icecandidate",
|
||||||
|
"t-on-iceconnectionstatechange",
|
||||||
|
"t-on-icegatheringstatechange",
|
||||||
|
"t-on-negotiationneeded",
|
||||||
|
"t-on-removestream",
|
||||||
|
"t-on-signalingstatechange",
|
||||||
|
"t-on-track",
|
||||||
|
"t-on-audioprocess",
|
||||||
|
"t-on-activate",
|
||||||
|
"t-on-contentdelete",
|
||||||
|
"t-on-install",
|
||||||
|
"t-on-notificationclick",
|
||||||
|
"t-on-pushsubscriptionchange",
|
||||||
|
"t-on-push",
|
||||||
|
"t-on-connect",
|
||||||
|
"t-on-audioend",
|
||||||
|
"t-on-audiostart",
|
||||||
|
"t-on-end",
|
||||||
|
"t-on-nomatch",
|
||||||
|
"t-on-result",
|
||||||
|
"t-on-soundend",
|
||||||
|
"t-on-soundstart",
|
||||||
|
"t-on-speechend",
|
||||||
|
"t-on-speechstart",
|
||||||
|
"t-on-start",
|
||||||
|
"t-on-voiceschanged",
|
||||||
|
"t-on-boundary",
|
||||||
|
"t-on-mark",
|
||||||
|
"t-on-resume",
|
||||||
|
"t-on-unload",
|
||||||
|
"t-on-afterprint",
|
||||||
|
"t-on-appinstalled",
|
||||||
|
"t-on-beforeprint",
|
||||||
|
"t-on-beforeunload",
|
||||||
|
"t-on-devicemotion",
|
||||||
|
"t-on-deviceorientation",
|
||||||
|
"t-on-gamepadconnected",
|
||||||
|
"t-on-gamepaddisconnected",
|
||||||
|
"t-on-hashchange",
|
||||||
|
"t-on-languagechange",
|
||||||
|
"t-on-offline",
|
||||||
|
"t-on-online",
|
||||||
|
"t-on-orientationchange",
|
||||||
|
"t-on-pagehide",
|
||||||
|
"t-on-pageshow",
|
||||||
|
"t-on-popstate",
|
||||||
|
"t-on-rejectionhandled",
|
||||||
|
"t-on-storage",
|
||||||
|
"t-on-unhandledrejection",
|
||||||
|
"t-on-vrdisplayactivate",
|
||||||
|
"t-on-vrdisplayblur",
|
||||||
|
"t-on-vrdisplayconnect",
|
||||||
|
"t-on-vrdisplaydeactivate",
|
||||||
|
"t-on-vrdisplaydisconnect",
|
||||||
|
"t-on-vrdisplayfocus",
|
||||||
|
"t-on-vrdisplaypointerrestricted",
|
||||||
|
"t-on-vrdisplaypointerunrestricted",
|
||||||
|
"t-on-vrdisplaypresentchange",
|
||||||
|
"t-on-timeout",
|
||||||
|
"t-on-inputsourceschange",
|
||||||
|
"t-on-selectend",
|
||||||
|
"t-on-squeezeend",
|
||||||
|
"t-on-squeezestart",
|
||||||
|
"t-on-squeeze"
|
||||||
|
].map(label => ({
|
||||||
|
label: label,
|
||||||
|
insertText: label + '=""',
|
||||||
|
kind: CompletionItemKind.Property,
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
To generate the elements list, run the following snippet on https://developer.mozilla.org/en-US/docs/Web/HTML/Element
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
const names = [...document.querySelectorAll("section:not([aria-labelledby='obsolete_and_deprecated_elements']) td:nth-child(1)")]
|
||||||
|
.flatMap(n => n.innerText.split(","))
|
||||||
|
.map(n => n.trim().replace("<", "").replace(">", ""))
|
||||||
|
return JSON.stringify([...new Set(names)], null, 2);
|
||||||
|
})()
|
||||||
|
*/
|
||||||
|
export const elements = [
|
||||||
|
"t",
|
||||||
|
"link",
|
||||||
|
"meta",
|
||||||
|
"style",
|
||||||
|
"title",
|
||||||
|
"body",
|
||||||
|
"address",
|
||||||
|
"article",
|
||||||
|
"aside",
|
||||||
|
"footer",
|
||||||
|
"header",
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"h5",
|
||||||
|
"h6",
|
||||||
|
"hgroup",
|
||||||
|
"main",
|
||||||
|
"nav",
|
||||||
|
"section",
|
||||||
|
"search",
|
||||||
|
"blockquote",
|
||||||
|
"dd",
|
||||||
|
"div",
|
||||||
|
"dl",
|
||||||
|
"dt",
|
||||||
|
"figcaption",
|
||||||
|
"figure",
|
||||||
|
"hr",
|
||||||
|
"li",
|
||||||
|
"menu",
|
||||||
|
"ol",
|
||||||
|
"p",
|
||||||
|
"pre",
|
||||||
|
"ul",
|
||||||
|
"a",
|
||||||
|
"abbr",
|
||||||
|
"b",
|
||||||
|
"bdi",
|
||||||
|
"bdo",
|
||||||
|
"br",
|
||||||
|
"cite",
|
||||||
|
"code",
|
||||||
|
"data",
|
||||||
|
"dfn",
|
||||||
|
"em",
|
||||||
|
"i",
|
||||||
|
"kbd",
|
||||||
|
"mark",
|
||||||
|
"q",
|
||||||
|
"rp",
|
||||||
|
"rt",
|
||||||
|
"ruby",
|
||||||
|
"s",
|
||||||
|
"samp",
|
||||||
|
"small",
|
||||||
|
"span",
|
||||||
|
"strong",
|
||||||
|
"sub",
|
||||||
|
"sup",
|
||||||
|
"time",
|
||||||
|
"u",
|
||||||
|
"var",
|
||||||
|
"wbr",
|
||||||
|
"area",
|
||||||
|
"audio",
|
||||||
|
"img",
|
||||||
|
"map",
|
||||||
|
"track",
|
||||||
|
"video",
|
||||||
|
"embed",
|
||||||
|
"iframe",
|
||||||
|
"object",
|
||||||
|
"picture",
|
||||||
|
"portal",
|
||||||
|
"source",
|
||||||
|
"svg",
|
||||||
|
"math",
|
||||||
|
"canvas",
|
||||||
|
"noscript",
|
||||||
|
"script",
|
||||||
|
"del",
|
||||||
|
"ins",
|
||||||
|
"caption",
|
||||||
|
"col",
|
||||||
|
"colgroup",
|
||||||
|
"table",
|
||||||
|
"tbody",
|
||||||
|
"td",
|
||||||
|
"tfoot",
|
||||||
|
"th",
|
||||||
|
"thead",
|
||||||
|
"tr",
|
||||||
|
"button",
|
||||||
|
"datalist",
|
||||||
|
"fieldset",
|
||||||
|
"form",
|
||||||
|
"input",
|
||||||
|
"label",
|
||||||
|
"legend",
|
||||||
|
"meter",
|
||||||
|
"optgroup",
|
||||||
|
"option",
|
||||||
|
"output",
|
||||||
|
"progress",
|
||||||
|
"select",
|
||||||
|
"textarea",
|
||||||
|
"details",
|
||||||
|
"dialog",
|
||||||
|
"summary",
|
||||||
|
].map(label => ({
|
||||||
|
label: label,
|
||||||
|
insertText: label,
|
||||||
|
kind: CompletionItemKind.Property,
|
||||||
|
}));
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
import { CancellationToken, CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, DefinitionProvider, Location, Position, Range, TextDocument, TextDocumentContentProvider, Uri, commands, workspace } from "vscode";
|
||||||
|
import { Search } from "../search";
|
||||||
|
import { getSelectedText, hash, readFile } from "../utils";
|
||||||
|
import { elements, events, owlComponentAttributes, owlElementAttributes } from "./items";
|
||||||
|
import { ParseResultType, ParseResult, parse, getNodePath, parseXml } from "./parser";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commands return basic js object which needs to be converted
|
||||||
|
* to actual CompletionItem instances, this methods streamlines
|
||||||
|
* this process.
|
||||||
|
*/
|
||||||
|
function mapCompletionItems(items: any): CompletionItem[] {
|
||||||
|
return items.map((i: any) => {
|
||||||
|
const item = new CompletionItem(i.label, i.kind);
|
||||||
|
item.sortText = i.sortText;
|
||||||
|
item.detail = i.detail;
|
||||||
|
item.filterText = i.filterText;
|
||||||
|
item.insertText = i.insertText?.startsWith?.(".") ? i.insertText.substring(1) : i.insertText;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterComponentItems(items: CompletionItem[], excludedLabels: string[] = []): CompletionItem[] {
|
||||||
|
return items.filter((item) => {
|
||||||
|
return !excludedLabels.includes(item.label.toString()) && [
|
||||||
|
CompletionItemKind.Field,
|
||||||
|
CompletionItemKind.Method,
|
||||||
|
CompletionItemKind.Variable,
|
||||||
|
CompletionItemKind.Property,
|
||||||
|
].includes(item.kind as number);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds "this." in front of the expression if needed and increments
|
||||||
|
* the expression offset accordingly.
|
||||||
|
*/
|
||||||
|
function contextualize(properties: string[], expression: string, expressionOffset = 0) {
|
||||||
|
const match = expression.match(/^([a-zA-Z_]+)\b/);
|
||||||
|
if (!expression.startsWith("this.") && ((match && properties.includes(match[1])) || expression.match(/^\s*$/))) {
|
||||||
|
expression = "this." + expression;
|
||||||
|
expressionOffset += 5;
|
||||||
|
}
|
||||||
|
return { expression, expressionOffset };
|
||||||
|
};
|
||||||
|
|
||||||
|
const Commands = {
|
||||||
|
Completion: "vscode.executeCompletionItemProvider",
|
||||||
|
Definition: "vscode.executeDefinitionProvider",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OwlLanguageFeaturesProvider implements CompletionItemProvider, TextDocumentContentProvider, DefinitionProvider {
|
||||||
|
|
||||||
|
virtualDocuments = new Map();
|
||||||
|
componentProperties = new Map();
|
||||||
|
search: Search;
|
||||||
|
|
||||||
|
constructor(search: Search) {
|
||||||
|
this.search = search;
|
||||||
|
workspace.registerTextDocumentContentProvider("owl", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextDocumentContentProvider interface implementation to provide
|
||||||
|
* virtual documents to vscode commands.
|
||||||
|
*/
|
||||||
|
async provideTextDocumentContent(uri: Uri) {
|
||||||
|
const id = uri.toString(true);
|
||||||
|
return this.virtualDocuments.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DefinitionProvider interface implementation.
|
||||||
|
*
|
||||||
|
* - If the target is a js expression, will try to find the definition
|
||||||
|
* inside the current component.
|
||||||
|
* - If the target is a component element, will try to find the definition
|
||||||
|
* of the component.
|
||||||
|
*/
|
||||||
|
async provideDefinition(document: TextDocument, position: Position) {
|
||||||
|
let offset = document.offsetAt(position);
|
||||||
|
const documentText = document.getText();
|
||||||
|
const parseResult = await parse(documentText, offset);
|
||||||
|
|
||||||
|
if (parseResult.type === ParseResultType.Expression) {
|
||||||
|
const component = await this.search.getCurrentComponent();
|
||||||
|
if (!component) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { xmlDocument, xmlNode } = parseXml(documentText, offset);
|
||||||
|
const componentText = await readFile(component.uri);
|
||||||
|
const virtualDocument = await this.getVirtualJsDocument(document.uri, component.componentName, componentText, xmlDocument, xmlNode, parseResult);
|
||||||
|
|
||||||
|
const definitions: any = await this.executeCommand(
|
||||||
|
Commands.Definition,
|
||||||
|
document.uri,
|
||||||
|
virtualDocument.content,
|
||||||
|
virtualDocument.offset
|
||||||
|
);
|
||||||
|
|
||||||
|
if (definitions.length > 0) {
|
||||||
|
const selectionRange = definitions[0].targetSelectionRange;
|
||||||
|
const range = new Range(
|
||||||
|
new Position(selectionRange.start.line, selectionRange.start.character),
|
||||||
|
new Position(selectionRange.end.line, selectionRange.end.character),
|
||||||
|
)
|
||||||
|
return new Location(component.uri, range);
|
||||||
|
}
|
||||||
|
} else if (parseResult.type === ParseResultType.Attribute) {
|
||||||
|
const { xmlNode } = parseXml(documentText, offset);
|
||||||
|
|
||||||
|
const childComponent = await this.search.findComponent(xmlNode.name);
|
||||||
|
if (!childComponent) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiersRegex = new RegExp([
|
||||||
|
"\\.bind",
|
||||||
|
"\\.stop",
|
||||||
|
"\\.prevent",
|
||||||
|
"\\.self",
|
||||||
|
"\\.capture",
|
||||||
|
"\\.sythetic",
|
||||||
|
].join("|"), "g");
|
||||||
|
|
||||||
|
let attributeName = getSelectedText(/\b[a-zA-Z0-9_\-.]+\b/, document, position)
|
||||||
|
attributeName = attributeName?.replace(modifiersRegex, "") ?? "";
|
||||||
|
|
||||||
|
const componentText = await readFile(childComponent.uri);
|
||||||
|
const content = `${componentText}\n${xmlNode.name}.props.${attributeName}`;
|
||||||
|
const definitions: any = await this.executeCommand(Commands.Definition, document.uri, content);
|
||||||
|
|
||||||
|
if (definitions.length > 0) {
|
||||||
|
const selectionRange = definitions[0].targetSelectionRange;
|
||||||
|
const range = new Range(
|
||||||
|
new Position(selectionRange.start.line, selectionRange.start.character),
|
||||||
|
new Position(selectionRange.end.line, selectionRange.end.character),
|
||||||
|
)
|
||||||
|
return new Location(childComponent.uri, range);
|
||||||
|
}
|
||||||
|
} else if (parseResult.type === ParseResultType.Element) {
|
||||||
|
const currentWord = getSelectedText(/<\/?[A-Z][a-zA-Z]+/, document, position);
|
||||||
|
if (!currentWord) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const componentName = currentWord.replace(/[\/<]/g, "").trim();
|
||||||
|
return await this.search.findComponent(componentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CompletionItemProvider interface implementation
|
||||||
|
*
|
||||||
|
* See {@link provideElementItems}, {@link provideAttributeItems} and {@link provideExpressionItems}
|
||||||
|
* for further details.
|
||||||
|
*/
|
||||||
|
async provideCompletionItems(
|
||||||
|
document: TextDocument,
|
||||||
|
position: Position,
|
||||||
|
token: CancellationToken,
|
||||||
|
context: CompletionContext
|
||||||
|
): Promise<CompletionItem[]> {
|
||||||
|
const component = await this.search.getCurrentComponent();
|
||||||
|
if (!component || token.isCancellationRequested) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = document.offsetAt(position);
|
||||||
|
const documentText = document.getText();
|
||||||
|
|
||||||
|
const parseResult = await parse(documentText, offset);
|
||||||
|
const { xmlDocument, xmlNode } = parseXml(documentText, offset);
|
||||||
|
|
||||||
|
if (parseResult.type === ParseResultType.Expression) {
|
||||||
|
return this.provideExpressionItems(document.uri, component.uri, component.componentName, xmlDocument, xmlNode, parseResult);
|
||||||
|
} else if (parseResult.type === ParseResultType.Attribute) {
|
||||||
|
return this.provideAttributeItems(document.uri, xmlNode);
|
||||||
|
} else if (parseResult.type === ParseResultType.Element) {
|
||||||
|
return this.provideElementItems(document.uri, component.uri, component.componentName, parseResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the completion items for attributes.
|
||||||
|
* - Returns props if the element is a component
|
||||||
|
* - Returns the owl directives based on the element type
|
||||||
|
*/
|
||||||
|
private async provideAttributeItems(
|
||||||
|
documentUri: Uri,
|
||||||
|
xmlNode: any,
|
||||||
|
): Promise<CompletionItem[]> {
|
||||||
|
if (!xmlNode || xmlNode.name === xmlNode.name.toLowerCase()) {
|
||||||
|
return [...owlElementAttributes, ...events];
|
||||||
|
}
|
||||||
|
|
||||||
|
const childComponent = await this.search.findComponent(xmlNode.name);
|
||||||
|
if (!childComponent) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentText = await readFile(childComponent.uri);
|
||||||
|
const content = `${componentText}\n${xmlNode.name}.props.`;
|
||||||
|
const list = await this.executeCommand(Commands.Completion, documentUri, content) as CompletionList;
|
||||||
|
|
||||||
|
const modifiersRegex = new RegExp([
|
||||||
|
"\\.bind",
|
||||||
|
"\\.stop",
|
||||||
|
"\\.prevent",
|
||||||
|
"\\.self",
|
||||||
|
"\\.capture",
|
||||||
|
"\\.sythetic",
|
||||||
|
].join("|"), "g");
|
||||||
|
|
||||||
|
const excludedAttrs = [
|
||||||
|
"slots",
|
||||||
|
...Object.keys(xmlNode.attr).map(attr => attr.replace(modifiersRegex, ""))
|
||||||
|
];
|
||||||
|
|
||||||
|
return mapCompletionItems(filterComponentItems([
|
||||||
|
...owlComponentAttributes,
|
||||||
|
...list.items
|
||||||
|
], excludedAttrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the completion items for elements, this includes
|
||||||
|
* components, "t" and html elements.
|
||||||
|
*/
|
||||||
|
private async provideElementItems(
|
||||||
|
documentUri: Uri,
|
||||||
|
componentUri: Uri,
|
||||||
|
componentName: string,
|
||||||
|
parseResult: any,
|
||||||
|
): Promise<CompletionItem[]> {
|
||||||
|
const componentText = await readFile(componentUri);
|
||||||
|
|
||||||
|
const content = `${componentText}\n${componentName}.components.${parseResult.expression}`;
|
||||||
|
const list = await this.executeCommand(Commands.Completion, documentUri, content) as CompletionList;
|
||||||
|
|
||||||
|
return mapCompletionItems([
|
||||||
|
...elements,
|
||||||
|
...filterComponentItems(list.items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the completion items for a js expression
|
||||||
|
*/
|
||||||
|
private async provideExpressionItems(
|
||||||
|
documentUri: Uri,
|
||||||
|
componentUri: Uri,
|
||||||
|
componentName: string,
|
||||||
|
xmlDocument: any,
|
||||||
|
xmlNode: any,
|
||||||
|
parseResult: ParseResult,
|
||||||
|
): Promise<CompletionItem[]> {
|
||||||
|
let { attributeName } = parseResult;
|
||||||
|
|
||||||
|
const dynamicAttributeRegex = new RegExp([
|
||||||
|
"t-if",
|
||||||
|
"t-elif",
|
||||||
|
"t-foreach",
|
||||||
|
"t-as",
|
||||||
|
"t-key",
|
||||||
|
"t-esc",
|
||||||
|
"t-out",
|
||||||
|
"t-props",
|
||||||
|
"t-component",
|
||||||
|
"t-set",
|
||||||
|
"t-value",
|
||||||
|
"t-portal",
|
||||||
|
"t-slot-scope",
|
||||||
|
"t-att",
|
||||||
|
"t-tag",
|
||||||
|
"t-log",
|
||||||
|
"t-model",
|
||||||
|
"t-att-[a-z_:.-]+",
|
||||||
|
"t-on-[a-z_:.-]+"
|
||||||
|
].join("|"));
|
||||||
|
|
||||||
|
if (xmlNode.name === xmlNode.name.toLowerCase() && !dynamicAttributeRegex.test(attributeName)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentText = await readFile(componentUri);
|
||||||
|
const virtualDocument = await this.getVirtualJsDocument(documentUri, componentName, componentText, xmlDocument, xmlNode, parseResult);
|
||||||
|
|
||||||
|
const completionList = await this.executeCommand(
|
||||||
|
Commands.Completion,
|
||||||
|
documentUri,
|
||||||
|
virtualDocument.content,
|
||||||
|
virtualDocument.offset
|
||||||
|
) as CompletionList;
|
||||||
|
|
||||||
|
let items = filterComponentItems(completionList.items, ["__VIRTUAL__", "setup"]);
|
||||||
|
|
||||||
|
if (!/\bthis\./.test(parseResult.expression)) {
|
||||||
|
items = items.map(item => {
|
||||||
|
let insertText = item.insertText as string;
|
||||||
|
if (/\bthis\./.test(insertText)) {
|
||||||
|
item.insertText = insertText.replace(/\bthis\./, "");
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapCompletionItems([...items]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a virtual document to provide the appropriate
|
||||||
|
* completion items for a parsed js expression.
|
||||||
|
*
|
||||||
|
* This method:
|
||||||
|
* - Adds default owl variables such as env and props
|
||||||
|
* - Adds mocks for frequently used Owl imports (which cannot be resolved using commands)
|
||||||
|
* - Adds local variables generated from Owl xml directives such as t-for or t-set
|
||||||
|
* - Will try to add a "this." in front of the js expression if it was omitted so it can
|
||||||
|
* be understood by vscode typescript server.
|
||||||
|
*
|
||||||
|
* The expression offset is also modified accordingly.
|
||||||
|
*/
|
||||||
|
async getVirtualJsDocument(
|
||||||
|
documentUri: Uri,
|
||||||
|
componentName: string,
|
||||||
|
componentText: string,
|
||||||
|
xmlDocument: any,
|
||||||
|
xmlNode: any,
|
||||||
|
parseResult: ParseResult,
|
||||||
|
) {
|
||||||
|
const properties = await this.getComponentProperties(documentUri, componentName, componentText);
|
||||||
|
|
||||||
|
// As imports do not work, use mocks for frequently used owl functions.
|
||||||
|
let importReplacements = workspace.getConfiguration().get(`owl-vision.autocomplete-mocks`);
|
||||||
|
|
||||||
|
// As imports do not work, manually add "env" and "props"
|
||||||
|
// to the current component instance.
|
||||||
|
const localVariables = [
|
||||||
|
"let env = {};",
|
||||||
|
"this.env = env;",
|
||||||
|
`let props = ${componentName}.props;`,
|
||||||
|
`this.props = props;`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Adds local variables generated based on the template
|
||||||
|
const path = getNodePath(xmlDocument, xmlNode);
|
||||||
|
for (const node of path) {
|
||||||
|
if (node.attr["t-foreach"]) {
|
||||||
|
let array = contextualize(properties, node.attr["t-foreach"]).expression;
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]} = ${array}[0];`);
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]}_index = 0;`);
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]}_first = ${array}[0];`);
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]}_last = ${array}.at(-1);`);
|
||||||
|
localVariables.push(`const ${node.attr["t-as"]}_value = {};`);
|
||||||
|
} else if (node.attr["t-set"]) {
|
||||||
|
localVariables.push(`const ${node.attr["t-set"]} = ${node.attr["t-value"]};`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { expression, expressionOffset } = contextualize(properties, parseResult.expression, parseResult.expressionOffset);
|
||||||
|
|
||||||
|
return {
|
||||||
|
offset: expressionOffset,
|
||||||
|
content: `${componentText}
|
||||||
|
${importReplacements}
|
||||||
|
class __VIRTUAL__ extends ${componentName} { __VIRTUAL__() {
|
||||||
|
${localVariables.join("\n")}
|
||||||
|
${expression} }}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of properties for a given component class.
|
||||||
|
* The result in cached in `componentProperties`.
|
||||||
|
*
|
||||||
|
* @param documentUri
|
||||||
|
* @param componentName
|
||||||
|
* @param componentText
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getComponentProperties(
|
||||||
|
documentUri: Uri,
|
||||||
|
componentName: string,
|
||||||
|
componentText: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const check = hash(componentText);
|
||||||
|
|
||||||
|
let cached = this.componentProperties.get(componentName);
|
||||||
|
if (!cached || cached.check !== check) {
|
||||||
|
const contextExpression = `${componentText}\nclass __VIRTUAL__ extends ${componentName} { __VIRTUAL__() { \nthis. }}`;
|
||||||
|
const contextItemsCompletion = await this.executeCommand(Commands.Completion, documentUri, contextExpression, 5) as CompletionList;
|
||||||
|
|
||||||
|
const items = filterComponentItems(contextItemsCompletion.items, ["__VIRTUAL__", "setup"]).map(item => item.label);
|
||||||
|
|
||||||
|
cached = { check, items };
|
||||||
|
this.componentProperties.set(componentName, cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeCommand(commandId: string, uri: Uri, content: string, offset: any = undefined) {
|
||||||
|
const lines = content.split(/\r\n|\r|\n/);
|
||||||
|
const _offset = offset !== undefined ? offset : (lines.at(-1)?.length ?? 0);
|
||||||
|
const position = new Position(lines.length - 1, _offset);
|
||||||
|
|
||||||
|
const originalUri = uri.toString(true);
|
||||||
|
const hashValue = hash(content);
|
||||||
|
|
||||||
|
const id = `owl://js/${originalUri}_${hashValue}.js`;
|
||||||
|
this.virtualDocuments.set(id, content);
|
||||||
|
|
||||||
|
return await commands.executeCommand(
|
||||||
|
commandId,
|
||||||
|
Uri.parse(`owl://js/${encodeURIComponent(originalUri)}_${hashValue}.js`),
|
||||||
|
position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { XmlDocument, XmlElement, XmlNode } from "xmldoc";
|
||||||
|
|
||||||
|
export enum ParseResultType {
|
||||||
|
Expression,
|
||||||
|
Attribute,
|
||||||
|
Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParseResult {
|
||||||
|
type: ParseResultType;
|
||||||
|
expression: string;
|
||||||
|
expressionOffset: number;
|
||||||
|
attributeName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will parse the document to find the selected expression based on an offset.
|
||||||
|
* The result can be on of three types:
|
||||||
|
*
|
||||||
|
* Element: The offset is on a element tag name, the expression is the current
|
||||||
|
* tagname or and empty string if it's just a opening tag.
|
||||||
|
*
|
||||||
|
* Attribute: The offset is inside the element but not in an attribute value,
|
||||||
|
* the expression is the current attribute name if any.
|
||||||
|
*
|
||||||
|
* Expression: The offset is inside an attribute value, the expression is the value.
|
||||||
|
*/
|
||||||
|
export async function parse(
|
||||||
|
documentText: string,
|
||||||
|
offset: number,
|
||||||
|
): Promise<ParseResult> {
|
||||||
|
|
||||||
|
// Check if the offset is preceded by "<xyz", if true returns a type Element
|
||||||
|
// with the current name.
|
||||||
|
const elementMatch = documentText.substring(0, offset).match(/<([a-zA-Z\-._]*)$/);
|
||||||
|
if (elementMatch) {
|
||||||
|
return {
|
||||||
|
type: ParseResultType.Element,
|
||||||
|
expression: elementMatch[1] || "",
|
||||||
|
expressionOffset: elementMatch[1].length || 0,
|
||||||
|
attributeName: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
value: expression,
|
||||||
|
offset: expressionOffset,
|
||||||
|
from,
|
||||||
|
} = getSection(documentText, offset, '="', '"');
|
||||||
|
|
||||||
|
// If the expression contains '"', it means we aren't inside an attribute
|
||||||
|
// value.
|
||||||
|
if (expression.includes('"')) {
|
||||||
|
const attributeMatch = documentText.substring(0, offset).match(/\s([a-zA-Z\-._]*)$/);
|
||||||
|
if (attributeMatch) {
|
||||||
|
return {
|
||||||
|
type: ParseResultType.Attribute,
|
||||||
|
expression: attributeMatch[1] || "",
|
||||||
|
expressionOffset: attributeMatch[1].length || 0,
|
||||||
|
attributeName: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributeName = "";
|
||||||
|
let i = from - 2;
|
||||||
|
while (/\S/.test(documentText[i])) {
|
||||||
|
attributeName = documentText[i] + attributeName;
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: ParseResultType.Expression,
|
||||||
|
expression,
|
||||||
|
expressionOffset,
|
||||||
|
attributeName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSection(text: string, offset: number, prefix: string, postfix: string) {
|
||||||
|
const beforeText = text.substring(0, offset);
|
||||||
|
let from = beforeText.lastIndexOf(prefix);
|
||||||
|
const afterText = text.substring(offset);
|
||||||
|
const to = beforeText.length + afterText.indexOf(postfix);
|
||||||
|
|
||||||
|
from = from + (prefix.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: text.substring(from, to),
|
||||||
|
offset: offset - from,
|
||||||
|
from: from,
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns 2 xml nodes:
|
||||||
|
* xmlNode: Tries to create the current element based on a string offset,
|
||||||
|
* even if the node is invalid.
|
||||||
|
* xmlDocument: The document root element, only works if the document is
|
||||||
|
* valid xml.
|
||||||
|
*/
|
||||||
|
export function parseXml(text: string, offset: number): any {
|
||||||
|
let xmlDocument = undefined;
|
||||||
|
try {
|
||||||
|
xmlDocument = new XmlDocument(text);
|
||||||
|
} catch (error) { }
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
while (text[offset + i] !== "<") {
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = "";
|
||||||
|
while (text[offset + i] !== ">" || text[offset + i - 1] === "=") {
|
||||||
|
node += text[offset + i];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let xmlNode = undefined;
|
||||||
|
try {
|
||||||
|
xmlNode = new XmlDocument(`${node}${node.endsWith("/") ? '' : '/'}>`);
|
||||||
|
} catch (error) { }
|
||||||
|
|
||||||
|
return { xmlDocument, xmlNode };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array representing the elements order from the document's
|
||||||
|
* root to the specified element.
|
||||||
|
*/
|
||||||
|
export function getNodePath(xmlDocument: XmlDocument, xmlNode: any): Array<XmlElement> {
|
||||||
|
let path: Array<XmlElement> = [];
|
||||||
|
|
||||||
|
const traverse = (node: XmlElement, currentPath: Array<XmlElement>) => {
|
||||||
|
if (node.name === xmlNode.name && JSON.stringify(node.attr) === JSON.stringify(xmlNode.attr)) {
|
||||||
|
path = currentPath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
if (child instanceof XmlElement && child.name) {
|
||||||
|
traverse(child, currentPath.concat(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(xmlDocument, []);
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
@@ -94,6 +94,31 @@ export class Search {
|
|||||||
return await this.find(templateName, query, "xml");
|
return await this.find(templateName, query, "xml");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getCurrentComponent(): Promise<any | undefined> {
|
||||||
|
if (!this.currentDocument) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = this.currentDocument.getText();
|
||||||
|
const templateName = this.getTemplateName(text, false);
|
||||||
|
|
||||||
|
if (templateName) {
|
||||||
|
const component = await this.findComponentFromTemplateName(templateName);
|
||||||
|
|
||||||
|
if (component) {
|
||||||
|
const componentFile = await workspace.fs.readFile(component.uri);
|
||||||
|
const componentText = Buffer.from(componentFile).toString('utf8');
|
||||||
|
const componentName = this.getComponentName(componentText, templateName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
uri: component.uri,
|
||||||
|
templateName,
|
||||||
|
componentName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private findComponentFromTemplateName(templateName: string): Promise<Location | undefined> {
|
private findComponentFromTemplateName(templateName: string): Promise<Location | undefined> {
|
||||||
const query = this.buildQuery(`template\\s*=\\s*["']`, templateName, `["']`);
|
const query = this.buildQuery(`template\\s*=\\s*["']`, templateName, `["']`);
|
||||||
return this.find(templateName, query, "js");
|
return this.find(templateName, query, "js");
|
||||||
@@ -107,6 +132,27 @@ export class Search {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getComponentName(str: string, templateName: string): string {
|
||||||
|
const templateNameRegex = new RegExp(`template\\s*=\\s*["'](${templateName})["']`, 'g');
|
||||||
|
const templateIndex = [...str.matchAll(templateNameRegex)][0]?.index ?? 0;
|
||||||
|
|
||||||
|
const matches = [...str.matchAll(new RegExp(`class\\s+([A-Za-z_]+)\\sextends\\s+[A-Za-z_]+`, 'g'))];
|
||||||
|
let result = "";
|
||||||
|
let currentIndex = -1;
|
||||||
|
for (const match of matches) {
|
||||||
|
if (match.index > templateIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.index > currentIndex) {
|
||||||
|
result = match[1];
|
||||||
|
currentIndex = match.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async find(
|
public async find(
|
||||||
name: string,
|
name: string,
|
||||||
searchQuery: string,
|
searchQuery: string,
|
||||||
|
|||||||
@@ -102,3 +102,20 @@ export async function showResult(result: vscode.Location, openDirection: OpenDir
|
|||||||
editor.revealRange(result.range);
|
editor.revealRange(result.range);
|
||||||
editor.selection = new vscode.Selection(result.range.start, result.range.end);
|
editor.selection = new vscode.Selection(result.range.start, result.range.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readFile(uri: vscode.Uri): Promise<string> {
|
||||||
|
const data = await vscode.workspace.fs.readFile(uri);
|
||||||
|
return Buffer.from(data).toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hash(str: string) {
|
||||||
|
var hash = 0,
|
||||||
|
i, chr;
|
||||||
|
if (str.length === 0) return hash;
|
||||||
|
for (i = 0; i < str.length; i++) {
|
||||||
|
chr = str.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + chr;
|
||||||
|
hash |= 0;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"contentName": "meta.embedded.block.javascript string.quoted.double.xml",
|
"contentName": "meta.embedded.block.javascript string.quoted.double.xml",
|
||||||
"begin": "(\\s*)(t-if|t-else|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(\")",
|
"begin": "(\\s*)(t-if|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att|t-tag|t-log|t-model|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(\")",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"2": {
|
"2": {
|
||||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"contentName": "meta.embedded.block.javascript string.quoted.single.xml",
|
"contentName": "meta.embedded.block.javascript string.quoted.single.xml",
|
||||||
"begin": "(\\s*)(t-if|t-else|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(')",
|
"begin": "(\\s*)(t-if|t-elif|t-foreach|t-as|t-key|t-esc|t-out|t-props|t-component|t-set|t-value|t-portal|t-slot-scope|t-att|t-tag|t-log|t-model|t-att-[a-z_:.-]+|t-on-[a-z_:.-]+)(=)(')",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"2": {
|
"2": {
|
||||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.dynamic"
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"contentName": "string.quoted.double.xml",
|
"contentName": "string.quoted.double.xml",
|
||||||
"begin": "(\\s*)(t-name|t-ref|t-set-slot|t-model|t-inherit|t-inherit-mode|t-translation)(=)(\")",
|
"begin": "(\\s*)(t-name|t-else|t-ref|t-set-slot|t-inherit|t-inherit-mode|t-translation|t-debug)(=)(\")",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"2": {
|
"2": {
|
||||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"contentName": "string.quoted.single.xml",
|
"contentName": "string.quoted.single.xml",
|
||||||
"begin": "(\\s*)(t-name|t-ref|t-set-slot|t-model|t-inherit|t-inherit-mode|t-translation)(=)(')",
|
"begin": "(\\s*)(t-name|t-else|t-ref|t-set-slot|t-inherit|t-inherit-mode|t-translation|t-debug)(=)(')",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"2": {
|
"2": {
|
||||||
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
"name": "entity.other.attribute-name.localname.xml owl.attribute owl.attribute.static"
|
||||||
|
|||||||
Reference in New Issue
Block a user