Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
## [Unreleased]
- Added: Class namespace autocomplete in XML files
- Added: Module name autocomplete in module.xml files
- Added: Module hover information
- Added: Added extension config fields for enabling/disabling completions, definitions and hovers
- Added: acl.xml indexer, definitions, autocomplete and hovers
- Added: Index data persistance
- Changed: Adjusted namespace indexer logic

Expand Down
146 changes: 146 additions & 0 deletions src/common/xml/XmlSuggestionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { minimatch } from 'minimatch';
import { Position, TextDocument, Range } from 'vscode';
import {
getSuggestions,
SuggestionProviders,
AttributeValueCompletionOptions,
ElementContentCompletionOptions,
} from '@xml-tools/content-assist';
import Config from 'common/Config';
import { TokenData } from 'common/xml/XmlDocumentParser';
import { XMLElement } from '@xml-tools/ast';
import { XMLAttribute } from '@xml-tools/ast';
import { MatchCondition } from './suggestion/condition/MatchCondition';

export type CombinedCondition = MatchCondition[];

export abstract class XmlSuggestionProvider<T> {
public abstract getFilePatterns(): string[];
public abstract getSuggestionItems(
value: string,
range: Range,
document: TextDocument,
element: XMLElement,
attribute?: XMLAttribute
): T[];

public getConfigKey(): string | undefined {
return undefined;
}

public getAttributeValueConditions(): CombinedCondition[] {
return [];
}

public getElementContentMatches(): CombinedCondition[] {
return [];
}

public async provideSuggestions(
document: TextDocument,
position: Position,
tokenData: TokenData
): Promise<T[]> {
if (!this.canProvideSuggestions(document)) {
return [];
}

return this.processSuggestions(document, position, tokenData);
}

public getSuggestionProviders(document: TextDocument): SuggestionProviders<T> {
return {
attributeValue: [options => this.getAttributeValueSuggestionProviders(document, options)],
elementContent: [options => this.getElementContentSuggestionProviders(document, options)],
};
}

public getAttributeValueSuggestionProviders(
document: TextDocument,
{ element, attribute }: AttributeValueCompletionOptions<undefined>
): T[] {
const match = this.getAttributeValueConditions().find(matchElement => {
return this.matchesConditions(matchElement, element, attribute);
});

if (!match) {
return [];
}

const value = attribute?.value || '';

const range = attribute
? new Range(
attribute.position.startLine - 1,
attribute.position.startColumn + 1 + (attribute.key?.length ?? 0),
attribute.position.endLine - 1,
attribute.position.endColumn - 1
)
: new Range(0, 0, 0, 0);

return this.getSuggestionItems(value, range, document, element, attribute);
}

public getElementContentSuggestionProviders(
document: TextDocument,
{ element }: ElementContentCompletionOptions<undefined>
): T[] {
const match = this.getElementContentMatches().find(matchElement => {
return this.matchesConditions(matchElement, element);
});

if (!match) {
return [];
}

const textContents = element.textContents.length > 0 ? element.textContents[0] : null;
const elementValue = textContents?.text ?? '';

const range = textContents
? new Range(
textContents.position.startLine - 1,
textContents.position.startColumn - 1,
textContents.position.endLine - 1,
textContents.position.endColumn
)
: new Range(0, 0, 0, 0);

return this.getSuggestionItems(elementValue, range, document, element);
}

protected matchesConditions(
conditions: CombinedCondition,
element: XMLElement,
attribute?: XMLAttribute
): boolean {
return conditions.every(condition => condition.match(element, attribute));
}

protected processSuggestions(
document: TextDocument,
position: Position,
tokenData: TokenData
): T[] {
const suggestions = getSuggestions({
...tokenData,
offset: document.offsetAt(position),
providers: this.getSuggestionProviders(document),
});

return suggestions;
}

public canProvideSuggestions(document: TextDocument): boolean {
if (this.getConfigKey()) {
const provideXmlSuggestions = Config.get<boolean>(this.getConfigKey()!);

if (!provideXmlSuggestions) {
return false;
}
}

return this.getFilePatterns().some(pattern =>
minimatch(document.uri.fsPath, pattern, { matchBase: true })
);
}
}
40 changes: 40 additions & 0 deletions src/common/xml/XmlSuggestionProviderProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CancellationToken, Position, TextDocument } from 'vscode';
import XmlDocumentParser, { TokenData } from 'common/xml/XmlDocumentParser';
import { XmlSuggestionProvider } from './XmlSuggestionProvider';

export abstract class XmlSuggestionProviderProcessor<T> {
public constructor(private providers: XmlSuggestionProvider<T>[]) {}

public async provideSuggestions(
document: TextDocument,
position: Position,
token: CancellationToken
): Promise<T[]> {
if (!this.providers.some(provider => provider.canProvideSuggestions(document))) {
return [];
}

const tokenData = await XmlDocumentParser.parse(document, true);

const providerCompletionItems = await Promise.all(
this.providers.map(provider =>
this.getProviderCompletionItems(provider, document, position, tokenData)
)
);

return providerCompletionItems.flat();
}

protected async getProviderCompletionItems(
provider: XmlSuggestionProvider<T>,
document: TextDocument,
position: Position,
tokenData: TokenData
): Promise<T[]> {
if (!provider.canProvideSuggestions(document)) {
return [];
}

return provider.provideSuggestions(document, position, tokenData);
}
}
14 changes: 14 additions & 0 deletions src/common/xml/suggestion/condition/AttributeNameMatches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { XMLAttribute, XMLElement } from '@xml-tools/ast';
import { MatchCondition } from './MatchCondition';

export class AttributeNameMatches implements MatchCondition {
public constructor(private readonly attributeName: string) {}

public match(element: XMLElement, attribute?: XMLAttribute): boolean {
if (!attribute) {
return false;
}

return attribute.key === this.attributeName;
}
}
15 changes: 15 additions & 0 deletions src/common/xml/suggestion/condition/ElementAttributeMatches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { XMLElement } from '@xml-tools/ast';
import { MatchCondition } from './MatchCondition';

export class ElementAttributeMatches implements MatchCondition {
public constructor(
private readonly attributeName: string,
private readonly attributeValue: string
) {}

public match(element: XMLElement): boolean {
return element.attributes.some(
attr => attr.key === this.attributeName && attr.value === this.attributeValue
);
}
}
10 changes: 10 additions & 0 deletions src/common/xml/suggestion/condition/ElementNameMatches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { XMLElement } from '@xml-tools/ast';
import { MatchCondition } from './MatchCondition';

export class ElementNameMatches implements MatchCondition {
public constructor(private readonly elementName: string) {}

public match(element: XMLElement): boolean {
return element.name === this.elementName;
}
}
5 changes: 5 additions & 0 deletions src/common/xml/suggestion/condition/MatchCondition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { XMLAttribute, XMLElement } from '@xml-tools/ast';

export interface MatchCondition {
match(element: XMLElement, attribute?: XMLAttribute): boolean;
}
14 changes: 14 additions & 0 deletions src/common/xml/suggestion/condition/ParentElementNameMatches.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { XMLElement } from '@xml-tools/ast';
import { MatchCondition } from './MatchCondition';

export class ParentElementNameMatches implements MatchCondition {
public constructor(private readonly elementName: string) {}

public match(element: XMLElement): boolean {
if (element.parent.type === 'XMLElement') {
return element.parent.name === this.elementName;
}

return false;
}
}
57 changes: 0 additions & 57 deletions src/completion/XmlCompletionProvider.ts

This file was deleted.

30 changes: 30 additions & 0 deletions src/completion/XmlCompletionProviderProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CompletionItemProvider } from 'vscode';

import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor';
import { CompletionItem, Position, TextDocument } from 'vscode';

import { CancellationToken } from 'vscode';
import { ModuleCompletionProvider } from './xml/ModuleCompletionProvider';
import { NamespaceCompletionProvider } from './xml/NamespaceCompletionProvider';
import { AclCompletionProvider } from './xml/AclCompletionProvider';

export class XmlCompletionProviderProcessor
extends XmlSuggestionProviderProcessor<CompletionItem>
implements CompletionItemProvider
{
public constructor() {
super([
new ModuleCompletionProvider(),
new NamespaceCompletionProvider(),
new AclCompletionProvider(),
]);
}

public async provideCompletionItems(
document: TextDocument,
position: Position,
token: CancellationToken
): Promise<CompletionItem[]> {
return this.provideSuggestions(document, position, token);
}
}
Loading