Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes for vanilla twig and import completions #59

Merged
merged 15 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
51 changes: 46 additions & 5 deletions packages/language-server/phpUtils/getTwigMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,55 @@

namespace Twiggy\Metadata;

use Twig\Loader\LoaderInterface;

// From \Twig\Loader\FileSystemLoader
moetelo marked this conversation as resolved.
Show resolved Hide resolved
function isAbsolutePath(string $file): bool {
return strspn($file, '/\\', 0, 1)
|| (\strlen($file) > 3 && ctype_alpha($file[0])
&& ':' === $file[1]
&& strspn($file, '/\\', 2, 1)
)
|| null !== parse_url($file, \PHP_URL_SCHEME)
;
}

/**
* Map supported loader namespaces to paths.
* @param array<string,string[]> &$loaderPaths
* @param LoaderInterface $loader Loader.
*/
function mapNamespaces(array &$loaderPaths, \Twig\Loader\LoaderInterface $loader) {
moetelo marked this conversation as resolved.
Show resolved Hide resolved
if ($loader instanceof \Twig\Loader\FilesystemLoader) {
$namespaces = $loader->getNamespaces();
$rootPath = getcwd() . \DIRECTORY_SEPARATOR;

foreach ($namespaces as $namespace) {
$ns_index = \Twig\Loader\FilesystemLoader::MAIN_NAMESPACE === $namespace
? ''
: ('@' . $namespace);

$loaderPaths[$ns_index] = [];
foreach ($loader->getPaths($namespace) as $path) {
$loaderPaths[$ns_index][] = realpath(
isAbsolutePath($path)
? $path
: $rootPath . $path
);
}
}
} else if ($loader instanceof \Twig\Loader\ChainLoader) {
moetelo marked this conversation as resolved.
Show resolved Hide resolved
foreach ($loader->getLoaders() as $subLoader) {
mapNamespaces($loaderPaths, $subLoader);
}
}

}

function getTwigMetadata(\Twig\Environment $twig, string $framework = ''): array {
$loaderPathsArray = [];
if ($framework !== 'craft') {
$twigLoader = $twig->getLoader();
$namespaces = $twigLoader->getNamespaces();
foreach ($namespaces as $namespace) {
$loaderPathsArray[$namespace] = $twigLoader->getPaths($namespace);
}
mapNamespaces($loaderPathsArray, $twig->getLoader());
}

$globals = $twig->getGlobals();
Expand Down
46 changes: 36 additions & 10 deletions packages/language-server/src/commands/ExecuteCommandProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,60 @@ import { isInsideHtmlRegion } from '../utils/node';
import { DocumentCache } from '../documents';

export enum Command {
DebugCommand = 'twiggy.debug-command',
IsInsideHtmlRegion = 'twiggy.is-inside-html-region',
}

const commands = new Map([
[Command.IsInsideHtmlRegion, isInsideHtmlRegion],
]);

export class ExecuteCommandProvider {
private commands: Map<Command, (...args: any) => Promise<any>>;

constructor(
connection: Connection,
private readonly documentCache: DocumentCache,
) {
this.commands = new Map<Command, (...args: any) => Promise<any>>([
[Command.DebugCommand, this.debugCommand],
[Command.IsInsideHtmlRegion, this.isInsideHtmlRegionCommand],
]);

connection.onExecuteCommand(
this.onExecuteCommand.bind(this),
);
}

async onExecuteCommand(params: ExecuteCommandParams) {
const [ commandName ] = params.command.split('(');
const command = commands.get(commandName as Command);
const command = this.commands.get(params.command as Command);

if (!command || !params.arguments) {
if (
!command ||
!params.arguments ||
params.arguments.length < 1 ||
'string' !== typeof params.arguments[0]
) {
return;
}

const [uri, position] = params.arguments as [DocumentUri, Position];
const document = await this.documentCache.get(uri);
const [ workspaceFolderPath, ...args ] = params.arguments as [string, ...any];
if (workspaceFolderPath !== this.documentCache.workspaceFolderPath) {
moetelo marked this conversation as resolved.
Show resolved Hide resolved
console.warn(
`Attempt to call command ${params.command} from invalid workspace`,
`expected ${this.documentCache.workspaceFolderPath}`,
`got ${workspaceFolderPath}`,
);
return;
}

return command(document, position);
return command(...args);
}

private debugCommand = async (...args: any) =>
console.info("Debug command is called:", args);

private isInsideHtmlRegionCommand = async (
uri: DocumentUri,
position: Position,
) => {
const document = await this.documentCache.get(uri);
return isInsideHtmlRegion(document, position)
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class CompletionProvider {
...await variableProperties(document, this.documentCache, cursorNode, this.#expressionTypeResolver, params.position),
...await templatePaths(
cursorNode,
params.position,
this.workspaceFolderPath,
this.#environment.templateMappings,
),
Expand Down
35 changes: 23 additions & 12 deletions packages/language-server/src/completions/template-paths.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CompletionItem, CompletionItemKind, DocumentUri } from 'vscode-languageserver';
import { CompletionItem, CompletionItemKind, Position } from 'vscode-languageserver';
import path from 'path';
import { SyntaxNode } from 'web-tree-sitter';
import {
Expand All @@ -7,11 +7,12 @@ import {
} from '../constants/template-usage';
import getTwigFiles from '../utils/getTwigFiles';
import { TemplatePathMapping } from '../twigEnvironment/types';
import { documentUriToFsPath } from '../utils/uri';
import { getStringNodeValue } from 'utils/node';

export async function templatePaths(
cursorNode: SyntaxNode,
workspaceFolderUri: DocumentUri,
position: Position,
workspaceFolderDirectory: string,
templateMappings: TemplatePathMapping[],
): Promise<CompletionItem[]> {
if (cursorNode.type !== 'string') {
Expand Down Expand Up @@ -53,21 +54,31 @@ export async function templatePaths(
node.parent?.childForFieldName('name')?.text === 'block' &&
cursorNode?.equals(node.namedChildren[1]))
) {
const workspaceFolderDirectory = documentUriToFsPath(workspaceFolderUri);

const completions: CompletionItem[] = [];

const nodeStringStart = cursorNode.startPosition.column + 1;
const nodeStringEnd = cursorNode.endPosition.column - 1;

const searchText = getStringNodeValue(cursorNode)
.substring(0, position.character - nodeStringStart);

for (const { namespace, directory } of templateMappings) {
const templatesDirectory = path.resolve(workspaceFolderDirectory, directory);
const templatesDirectory = path.resolve(workspaceFolderDirectory, directory)

for (const twigPath of await getTwigFiles(directory)) {
const relativePathToTwigFromTemplatesDirectory = path.relative(templatesDirectory, twigPath);
const includePath = path.join(namespace, relativePathToTwigFromTemplatesDirectory);
const relativePathToTwigFromTemplatesDirectory = path.relative(templatesDirectory, twigPath)
// fix paths for win32
.replaceAll(path.sep, path.posix.sep);
moetelo marked this conversation as resolved.
Show resolved Hide resolved
const includePath = path.posix.join(namespace, relativePathToTwigFromTemplatesDirectory)

completions.push({
label: includePath,
kind: CompletionItemKind.File,
});
if (searchText === '' || includePath.startsWith(searchText)) {
completions.push({
label: includePath,
kind: CompletionItemKind.File,
// Send insertion range to extension
data: [ nodeStringStart, nodeStringEnd, ],
moetelo marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions packages/language-server/src/configuration/ConfigurationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { TypeResolver } from '../typing/TypeResolver';
import { isFile } from '../utils/files/fileStat';
import { readFile } from 'fs/promises';
import { DiagnosticProvider } from 'diagnostics';

export class ConfigurationManager {
readonly configurationSection = 'twiggy';
Expand All @@ -45,6 +46,7 @@ export class ConfigurationManager {
private readonly signatureHelpProvider: SignatureHelpProvider,
private readonly documentCache: DocumentCache,
private readonly workspaceFolder: WorkspaceFolder,
private readonly diagnosticProvider: DiagnosticProvider,
) {
connection.client.register(DidChangeConfigurationNotification.type, { section: this.configurationSection });
connection.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this));
Expand Down Expand Up @@ -83,6 +85,13 @@ export class ConfigurationManager {
workspaceDirectory,
});

if (null === twigEnvironment.environment) {
console.warn('Failed to load Twig environment.')
} else {
console.info('Successfully loaded Twig environment.')
console.debug(twigEnvironment.environment)
}

this.applySettings(twigEnvironment, phpExecutor);
}

Expand Down Expand Up @@ -117,6 +126,7 @@ export class ConfigurationManager {

this.definitionProvider.phpExecutor = phpExecutor;
this.completionProvider.refresh(frameworkEnvironment, phpExecutor, typeResolver);
this.diagnosticProvider.refresh(frameworkEnvironment, phpExecutor, typeResolver);
this.signatureHelpProvider.reindex(frameworkEnvironment);
this.documentCache.configure(frameworkEnvironment, typeResolver);
}
Expand Down
111 changes: 49 additions & 62 deletions packages/language-server/src/definitions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,60 +5,15 @@ import {
Range,
WorkspaceFolder,
} from 'vscode-languageserver';
import { getNodeRange } from '../utils/node';
import { SyntaxNode } from 'web-tree-sitter';
import {
templateUsingFunctions,
templateUsingStatements,
} from '../constants/template-usage';
import { getNodeRange, isBlockIdentifier, isPathInsideTemplateEmbedding } from '../utils/node';
import { Document, DocumentCache } from '../documents';
import { getStringNodeValue } from '../utils/node';
import { rangeContainsPosition, pointToPosition } from '../utils/position';
import { parseFunctionCall } from '../utils/node/parseFunctionCall';
import { pointToPosition } from '../utils/position';
import { positionsEqual } from '../utils/position/comparePositions';
import { documentUriToFsPath } from '../utils/uri';
import { PhpExecutor } from '../phpInterop/PhpExecutor';
import { findParentByType } from '../utils/node/findParentByType';

const isPathInsideTemplateEmbedding = (node: SyntaxNode): boolean => {
if (node.type !== 'string' || !node.parent) {
return false;
}

const isInsideStatement = templateUsingStatements.includes(
node.parent.type,
);

if (isInsideStatement) {
return true;
}

const isInsideFunctionCall =
node.parent?.type === 'arguments' &&
templateUsingFunctions.some((func) =>
parseFunctionCall(node.parent!.parent)?.name === func,
);

return isInsideFunctionCall;
};

const isBlockIdentifier = (node: SyntaxNode): boolean => {
if (!node.parent) {
return false;
}

if (node.parent.type === 'block' && node.type === 'identifier') {
return true;
}

if (node.parent.parent?.type === 'call_expression') {
const call = parseFunctionCall(node.parent.parent);
return !!call && node.type === 'string' && call.name === 'block' && !call.object;
}

return false;
};

export class DefinitionProvider {
workspaceFolderPath: string;
phpExecutor: PhpExecutor | null = null;
Expand Down Expand Up @@ -96,29 +51,61 @@ export class DefinitionProvider {
return {
uri: document.uri,
range: Range.create(0, 0, 0, 0),
// range: Range.create(
moetelo marked this conversation as resolved.
Show resolved Hide resolved
// cursorNode.startPosition.row, cursorNode.startPosition.column,
// cursorNode.endPosition.row, cursorNode.endPosition.column,
// ),
};
}

if (isBlockIdentifier(cursorNode)) {
const blockName = cursorNode.type === 'string'
? getStringNodeValue(cursorNode)
: cursorNode.text;

let extendedDocument: Document | undefined = document;
while (extendedDocument) {
const blockSymbol = extendedDocument.getBlock(blockName);
if (!blockSymbol || positionsEqual(blockSymbol.nameRange.start, getNodeRange(cursorNode).start)) {
extendedDocument = await this.getExtendedTemplate(extendedDocument);
continue;
if (cursorNode.parent?.type !== 'block') {
moetelo marked this conversation as resolved.
Show resolved Hide resolved
moetelo marked this conversation as resolved.
Show resolved Hide resolved
let extendedDocument: Document | undefined = document;

const blockArgumentNode = cursorNode.parent!.namedChildren[0];
const templateArgumentNode = cursorNode.parent!.namedChildren[1];
moetelo marked this conversation as resolved.
Show resolved Hide resolved

const blockName = blockArgumentNode.type === 'string'
? getStringNodeValue(blockArgumentNode)
: blockArgumentNode.text;

if (templateArgumentNode) {
const path = getStringNodeValue(templateArgumentNode);
const document = await this.documentCache.resolveByTwigPath(path);

if (!document) {
// target template not found
return;
}

if (cursorNode.equals(templateArgumentNode)) {
return {
uri: document.uri,
range: Range.create(0, 0, 0, 0),
// range: Range.create(
// templateArgumentNode.startPosition.row, templateArgumentNode.startPosition.column,
// templateArgumentNode.endPosition.row, templateArgumentNode.endPosition.column,
// ),
};
}
extendedDocument = document;
}

return {
uri: extendedDocument.uri,
range: blockSymbol.nameRange,
};
while (extendedDocument) {
const blockSymbol = extendedDocument.getBlock(blockName);
if (!blockSymbol || positionsEqual(blockSymbol.nameRange.start, getNodeRange(cursorNode).start)) {
extendedDocument = await this.getExtendedTemplate(extendedDocument);
continue;
}

return {
uri: extendedDocument.uri,
range: blockSymbol.nameRange,
};
}
}

return;

}

if (cursorNode.type === 'variable') {
Expand Down
Loading