Skip to content

Commit cfd94d4

Browse files
committed
feat: acl indexer, definitions, hovers, autocomplete; suggestion refactoring
1 parent 5eb6b4a commit cfd94d4

27 files changed

+814
-455
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
77
## [Unreleased]
88
- Added: Class namespace autocomplete in XML files
99
- Added: Module name autocomplete in module.xml files
10+
- Added: Module hover information
1011
- Added: Added extension config fields for enabling/disabling completions, definitions and hovers
12+
- Added: acl.xml indexer, definitions, autocomplete and hovers
1113
- Added: Index data persistance
1214
- Changed: Adjusted namespace indexer logic
1315

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { minimatch } from 'minimatch';
2+
import { Position, TextDocument, Range } from 'vscode';
3+
import {
4+
getSuggestions,
5+
SuggestionProviders,
6+
AttributeValueCompletionOptions,
7+
ElementContentCompletionOptions,
8+
} from '@xml-tools/content-assist';
9+
import Config from 'common/Config';
10+
import { TokenData } from 'common/xml/XmlDocumentParser';
11+
import { XMLElement } from '@xml-tools/ast';
12+
import { XMLAttribute } from '@xml-tools/ast';
13+
import { MatchCondition } from './suggestion/condition/MatchCondition';
14+
15+
export type CombinedCondition = MatchCondition[];
16+
17+
export abstract class XmlSuggestionProvider<T> {
18+
public abstract getFilePatterns(): string[];
19+
public abstract getSuggestionItems(
20+
value: string,
21+
range: Range,
22+
document: TextDocument,
23+
element: XMLElement,
24+
attribute?: XMLAttribute
25+
): T[];
26+
27+
public getConfigKey(): string | undefined {
28+
return undefined;
29+
}
30+
31+
public getAttributeValueConditions(): CombinedCondition[] {
32+
return [];
33+
}
34+
35+
public getElementContentMatches(): CombinedCondition[] {
36+
return [];
37+
}
38+
39+
public async provideSuggestions(
40+
document: TextDocument,
41+
position: Position,
42+
tokenData: TokenData
43+
): Promise<T[]> {
44+
if (!this.canProvideSuggestions(document)) {
45+
return [];
46+
}
47+
48+
return this.processSuggestions(document, position, tokenData);
49+
}
50+
51+
public getSuggestionProviders(document: TextDocument): SuggestionProviders<T> {
52+
return {
53+
attributeValue: [options => this.getAttributeValueSuggestionProviders(document, options)],
54+
elementContent: [options => this.getElementContentSuggestionProviders(document, options)],
55+
};
56+
}
57+
58+
public getAttributeValueSuggestionProviders(
59+
document: TextDocument,
60+
{ element, attribute }: AttributeValueCompletionOptions<undefined>
61+
): T[] {
62+
const match = this.getAttributeValueConditions().find(matchElement => {
63+
return this.matchesConditions(matchElement, element, attribute);
64+
});
65+
66+
if (!match) {
67+
return [];
68+
}
69+
70+
const value = attribute?.value || '';
71+
72+
const range = attribute
73+
? new Range(
74+
attribute.position.startLine - 1,
75+
attribute.position.startColumn + 1 + (attribute.key?.length ?? 0),
76+
attribute.position.endLine - 1,
77+
attribute.position.endColumn - 1
78+
)
79+
: new Range(0, 0, 0, 0);
80+
81+
return this.getSuggestionItems(value, range, document, element, attribute);
82+
}
83+
84+
public getElementContentSuggestionProviders(
85+
document: TextDocument,
86+
{ element }: ElementContentCompletionOptions<undefined>
87+
): T[] {
88+
const match = this.getElementContentMatches().find(matchElement => {
89+
return this.matchesConditions(matchElement, element);
90+
});
91+
92+
if (!match) {
93+
return [];
94+
}
95+
96+
const textContents = element.textContents.length > 0 ? element.textContents[0] : null;
97+
const elementValue = textContents?.text ?? '';
98+
99+
const range = textContents
100+
? new Range(
101+
textContents.position.startLine - 1,
102+
textContents.position.startColumn - 1,
103+
textContents.position.endLine - 1,
104+
textContents.position.endColumn
105+
)
106+
: new Range(0, 0, 0, 0);
107+
108+
return this.getSuggestionItems(elementValue, range, document, element);
109+
}
110+
111+
protected matchesConditions(
112+
conditions: CombinedCondition,
113+
element: XMLElement,
114+
attribute?: XMLAttribute
115+
): boolean {
116+
return conditions.every(condition => condition.match(element, attribute));
117+
}
118+
119+
protected processSuggestions(
120+
document: TextDocument,
121+
position: Position,
122+
tokenData: TokenData
123+
): T[] {
124+
const suggestions = getSuggestions({
125+
...tokenData,
126+
offset: document.offsetAt(position),
127+
providers: this.getSuggestionProviders(document),
128+
});
129+
130+
return suggestions;
131+
}
132+
133+
public canProvideSuggestions(document: TextDocument): boolean {
134+
if (this.getConfigKey()) {
135+
const provideXmlSuggestions = Config.get<boolean>(this.getConfigKey()!);
136+
137+
if (!provideXmlSuggestions) {
138+
return false;
139+
}
140+
}
141+
142+
return this.getFilePatterns().some(pattern =>
143+
minimatch(document.uri.fsPath, pattern, { matchBase: true })
144+
);
145+
}
146+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { CancellationToken, Position, TextDocument } from 'vscode';
2+
import XmlDocumentParser, { TokenData } from 'common/xml/XmlDocumentParser';
3+
import { XmlSuggestionProvider } from './XmlSuggestionProvider';
4+
5+
export abstract class XmlSuggestionProviderProcessor<T> {
6+
public constructor(private providers: XmlSuggestionProvider<T>[]) {}
7+
8+
public async provideSuggestions(
9+
document: TextDocument,
10+
position: Position,
11+
token: CancellationToken
12+
): Promise<T[]> {
13+
if (!this.providers.some(provider => provider.canProvideSuggestions(document))) {
14+
return [];
15+
}
16+
17+
const tokenData = await XmlDocumentParser.parse(document, true);
18+
19+
const providerCompletionItems = await Promise.all(
20+
this.providers.map(provider =>
21+
this.getProviderCompletionItems(provider, document, position, tokenData)
22+
)
23+
);
24+
25+
return providerCompletionItems.flat();
26+
}
27+
28+
protected async getProviderCompletionItems(
29+
provider: XmlSuggestionProvider<T>,
30+
document: TextDocument,
31+
position: Position,
32+
tokenData: TokenData
33+
): Promise<T[]> {
34+
if (!provider.canProvideSuggestions(document)) {
35+
return [];
36+
}
37+
38+
return provider.provideSuggestions(document, position, tokenData);
39+
}
40+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { XMLAttribute, XMLElement } from '@xml-tools/ast';
2+
import { MatchCondition } from './MatchCondition';
3+
4+
export class AttributeNameMatches implements MatchCondition {
5+
public constructor(private readonly attributeName: string) {}
6+
7+
public match(element: XMLElement, attribute?: XMLAttribute): boolean {
8+
if (!attribute) {
9+
return false;
10+
}
11+
12+
return attribute.key === this.attributeName;
13+
}
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { XMLElement } from '@xml-tools/ast';
2+
import { MatchCondition } from './MatchCondition';
3+
4+
export class ElementAttributeMatches implements MatchCondition {
5+
public constructor(
6+
private readonly attributeName: string,
7+
private readonly attributeValue: string
8+
) {}
9+
10+
public match(element: XMLElement): boolean {
11+
return element.attributes.some(
12+
attr => attr.key === this.attributeName && attr.value === this.attributeValue
13+
);
14+
}
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { XMLElement } from '@xml-tools/ast';
2+
import { MatchCondition } from './MatchCondition';
3+
4+
export class ElementNameMatches implements MatchCondition {
5+
public constructor(private readonly elementName: string) {}
6+
7+
public match(element: XMLElement): boolean {
8+
return element.name === this.elementName;
9+
}
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { XMLAttribute, XMLElement } from '@xml-tools/ast';
2+
3+
export interface MatchCondition {
4+
match(element: XMLElement, attribute?: XMLAttribute): boolean;
5+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { XMLElement } from '@xml-tools/ast';
2+
import { MatchCondition } from './MatchCondition';
3+
4+
export class ParentElementNameMatches implements MatchCondition {
5+
public constructor(private readonly elementName: string) {}
6+
7+
public match(element: XMLElement): boolean {
8+
if (element.parent.type === 'XMLElement') {
9+
return element.parent.name === this.elementName;
10+
}
11+
12+
return false;
13+
}
14+
}

src/completion/XmlCompletionProvider.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { CompletionItemProvider } from 'vscode';
2+
3+
import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor';
4+
import { CompletionItem, Position, TextDocument } from 'vscode';
5+
6+
import { CancellationToken } from 'vscode';
7+
import { ModuleCompletionProvider } from './xml/ModuleCompletionProvider';
8+
import { NamespaceCompletionProvider } from './xml/NamespaceCompletionProvider';
9+
import { AclCompletionProvider } from './xml/AclCompletionProvider';
10+
11+
export class XmlCompletionProviderProcessor
12+
extends XmlSuggestionProviderProcessor<CompletionItem>
13+
implements CompletionItemProvider
14+
{
15+
public constructor() {
16+
super([
17+
new ModuleCompletionProvider(),
18+
new NamespaceCompletionProvider(),
19+
new AclCompletionProvider(),
20+
]);
21+
}
22+
23+
public async provideCompletionItems(
24+
document: TextDocument,
25+
position: Position,
26+
token: CancellationToken
27+
): Promise<CompletionItem[]> {
28+
return this.provideSuggestions(document, position, token);
29+
}
30+
}

0 commit comments

Comments
 (0)