Skip to content

Commit

Permalink
update for completion provider and definition provider
Browse files Browse the repository at this point in the history
  • Loading branch information
hangxingliu committed Nov 18, 2021
1 parent af79234 commit 77c5d39
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 154 deletions.
2 changes: 2 additions & 0 deletions docs/TODO.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# TODO

- [ ] improve snippets
- [ ] improve formatter
- [ ] add completion for `location`, `if`
- [x] cache the block name cursor located in
- [x] make hint senmantic
- reference: <https://github.com/tmont/nginx-conf>
Expand Down
59 changes: 0 additions & 59 deletions src/extension/providers/completion-item.ts

This file was deleted.

25 changes: 25 additions & 0 deletions src/extension/providers/completion/directive-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SnippetString } from "vscode";
import { NginxDirective } from "../../hint-data/types";
import { getDirectiveCompletionItemBase } from "./item";

/**
* https://nginx.org/en/docs/http/ngx_http_core_module.html#location
* https://code.visualstudio.com/docs/editor/userdefinedsnippets
*/
const locationSyntax: Array<[string, SnippetString]> = [
["location", new SnippetString("location ${1:/} {\n\t$0\n}")],
["location (named)", new SnippetString("location @${1:name} {\n\t$0\n}")],
["location (exact match)", new SnippetString("location = ${1:/uri} {\n\t$0\n}")],
["location (regexp)", new SnippetString("location ~* ${1:/uri} {\n\t$0\n}")],
["location (regexp, case-sensitive)", new SnippetString("location ~ ${1:/uri} {\n\t$0\n}")],
["location (^~)", new SnippetString("location ^~ ${1:/uri} {\n\t$0\n}")],
];

export function getDirectiveLocationCItem(directive: NginxDirective) {
return locationSyntax.map((it) => {
const citem = getDirectiveCompletionItemBase(directive);
citem.label = it[0];
citem.insertText = it[1];
return citem;
});
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */

import {
CancellationToken,
CompletionContext,
Expand All @@ -9,27 +11,29 @@ import {
Position,
TextDocument,
Uri,
workspace,
FileType,
CompletionItemKind,
CompletionList,
} from "vscode";
import { getDirectivesManifest, getVariablesManifest } from "../hint-data/manifest";
import { NginxDirective } from "../hint-data/types";
import { logger } from "../logger";
import { getNginxConfCursorContext } from "../parser";
import { getDirectiveCompletionItemBase, getVariableCompletionItemBase } from "./completion-item";
import { extensionConfig } from "./config";
import { DOCUMENT_SELECTOR } from "./utils";
import { mediaTypePrefixes, mediaTypePrefixSet, mediaTypes, MediaTypeTuple } from "../hint-data/media-types";
import { getDirectivesManifest, getVariablesManifest } from "../../hint-data/manifest";
import { NginxDirective } from "../../hint-data/types";
import { logger } from "../../logger";
import { getNginxConfCursorContext, getNginxConfDefinitionInfo } from "../../parser";
import { getDirectiveCompletionItemBase, getVariableCompletionItemBase, resolveDirectiveCompletionItem } from "./item";
import { extensionConfig } from "../config";
import { DOCUMENT_SELECTOR } from "../utils";
import { mediaTypePrefixes, mediaTypePrefixSet, mediaTypes, MediaTypeTuple } from "../../hint-data/media-types";
import { _completePath, _doesDirectiveNeedCompletePath } from "./path";
import { getDirectiveLocationCItem } from "./directive-location";
import { _completeNameArgs } from "./named-arguments";
import { _completeNameLocation, _doesDirectiveCanUseNamedLocation } from "./named-location";

const zeroPos = new Position(0, 0);

export class NginxCompletionItemsProvider implements CompletionItemProvider {
mediaTypePrefixItems: CompletionItem[] = [];

constructor(disposables: Disposable[]) {
disposables.push(languages.registerCompletionItemProvider(DOCUMENT_SELECTOR, this, "$", "/", " "));
disposables.push(languages.registerCompletionItemProvider(DOCUMENT_SELECTOR, this, "$", "/", " ",'@'));
this.mediaTypePrefixItems = mediaTypePrefixes.map((it) => {
const item = new CompletionItem(`${it}/`, CompletionItemKind.Value);
item.detail = "Media Type";
Expand Down Expand Up @@ -92,19 +96,33 @@ export class NginxCompletionItemsProvider implements CompletionItemProvider {
if (it.name.startsWith(inputPrefix)) matchedDirectives.push(it);
});
}
};
}; // end of addDirectives

const manifest = getDirectivesManifest();
addDirectives(manifest.core);
if (extensionConfig.hasJsModule) addDirectives(manifest.js);
if (extensionConfig.hasLuaModule) addDirectives(manifest.lua);
return matchedDirectives.map(getDirectiveCompletionItemBase);

const result: CompletionItem[] = [];
for (let i = 0; i < matchedDirectives.length; i++) {
const directive = matchedDirectives[i];
switch (directive.name) {
case "location":
result.push(...getDirectiveLocationCItem(directive));
break;
default:
result.push(getDirectiveCompletionItemBase(directive));
break;
}
}
return result;
}

const currentInput = n || list.length === 0 ? "" : list[list.length - 1];

// include conf
if (list[0] == "include" && (n || list.length > 1)) {
return this.completePath(Uri.joinPath(document.uri, ".."), currentInput);
// complete file
if (_doesDirectiveNeedCompletePath(list[0]) && (n || list.length > 1)) {
return _completePath(Uri.joinPath(document.uri, ".."), currentInput);
}

// variable
Expand All @@ -115,10 +133,17 @@ export class NginxCompletionItemsProvider implements CompletionItemProvider {
if (extensionConfig.hasLuaModule) list = list.concat(variables.lua.map(getVariableCompletionItemBase));
return list;
}

// other args
if (n && !list[0].startsWith("$")) return _completeNameArgs(list[0]);

// use named location
if (currentInput[0] === '@' && _doesDirectiveCanUseNamedLocation(list[0]))
return _completeNameLocation(document, currentInput);
}

async resolveCompletionItem?(item: CompletionItem) {
return item;
async resolveCompletionItem(item: CompletionItem) {
return resolveDirectiveCompletionItem(item);
}

private async completeMediaType(input: string) {
Expand All @@ -138,26 +163,4 @@ export class NginxCompletionItemsProvider implements CompletionItemProvider {
}
return this.mediaTypePrefixItems;
}

private async completePath(baseUri: Uri, input = "") {
if (/[\/\\]$/.test(input)) input = input.slice(0, input.length - 1) + "/";
else input += "/..";

const inputUri = input ? Uri.joinPath(baseUri, input) : baseUri;

const result: CompletionItem[] = [];
try {
const files = await workspace.fs.readDirectory(inputUri);
files
.filter((it) => it[1] === FileType.Directory)
.forEach((it) => result.push(new CompletionItem(it[0], CompletionItemKind.Folder)));
files
.filter((it) => it[1] === FileType.File)
.forEach((it) => result.push(new CompletionItem(it[0], CompletionItemKind.File)));
} catch (error) {
// noop
}

return result;
}
}
71 changes: 71 additions & 0 deletions src/extension/providers/completion/item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { CompletionItem, CompletionItemKind, MarkdownString, SnippetString } from "vscode";
import { getModuleDetails } from "../../hint-data/details";
import { NginxDirective, NginxVariable } from "../../hint-data/types";

export type DirectiveCompletionItemBase = CompletionItem & {
resolved?: boolean;
module?: string;
directive?: string;
filter?: string[];
contexts?: string[];
};

export function getDirectiveCompletionItemBase(directive: NginxDirective) {
const moduleName = directive.module;
const isCoreFunc = moduleName == "ngx_core_module";

const item: DirectiveCompletionItemBase = new CompletionItem(directive.name, CompletionItemKind.Property);

const documentation = ["``` NGINX", directive.syntax.join("\n"), "```\n"].join("\n");
item.documentation = new MarkdownString(documentation);
item.detail = isCoreFunc ? "" : moduleName;
item.module = directive.module;
item.directive = directive.name;

//#region insertText
if (directive.ci?.insert) {
item.insertText = new SnippetString(directive.ci.insert);
} else {
item.insertText = new SnippetString(
directive.def // has default value
? directive.def.replace(/^(\w+)(\s+)(.+);$/, (_, a, b, c) => `${a}${b}\${1:${c}};`)
: `${directive.name}\$\{0\};`
);
}
//#endregion insertText

//for fuzzy matching
item.filter = directive.name.split("_");
//for checking parent block name
item.contexts = directive.contexts || [];
return item;
}

export async function resolveDirectiveCompletionItem(item: DirectiveCompletionItemBase) {
if (!item || !item.module || !item.directive || item.resolved) return item;
const moduleDetails = await getModuleDetails(item.module);
if (moduleDetails) {
const d = moduleDetails.diretives.get(item.directive);
if (item.documentation instanceof MarkdownString) {
item.documentation = item.documentation.appendMarkdown(d.markdown);
}
item.resolved = true;
}
return item;
}

function removeUglyCharactersInCompletionItem(text = "") {
return (
text
.replace(/&#x(\w{4});/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
//remove <code></code> mark
.replace(/“`(.+?)`”/g, "“$1”")
);
}
export function getVariableCompletionItemBase(variable: NginxVariable) {
const item = new CompletionItem(variable.name, CompletionItemKind.Variable);
item.documentation = removeUglyCharactersInCompletionItem(variable.desc);
item.detail = variable.module;
item.insertText = variable.name.replace(/^\$/, "");
return item;
}
36 changes: 36 additions & 0 deletions src/extension/providers/completion/named-arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { CompletionItem, CompletionItemKind, SnippetString } from "vscode";
import { findManifestByName } from "../../hint-data/manifest";
import { NginxDirective } from "../../hint-data/types";

export function _completeNameArgs(directiveName: string) {
const directives = findManifestByName(directiveName) as NginxDirective[];
/** key is args, value is module name */
const result = new Map<string, string>();
for (let i = 0; i < directives.length; i++) {
const directive = directives[i];
const args = directive.ci?.args;
if (!args) continue;
for (let j = 0; j < args.length; j++) {
const moduleName = result.get(args[j]);
if (moduleName) result.set(args[j], moduleName + "," + directive.module);
else result.set(args[j], moduleName);
}
}

if (result.size > 0) {
const items: CompletionItem[] = [];
const args = Array.from(result.entries());
for (let j = 0; j < args.length; j++) {
const [arg, moduleName] = args[j];
const part = arg.match(/^(\??)(\w+)=(.+)$/);
if (!part) continue;
const item = new CompletionItem(`${part[2]}=`, CompletionItemKind.Field);
item.detail = moduleName;
item.insertText = new SnippetString(
part[1] === "?" ? `${part[2]}\${1:=${part[3]}}` : `${part[2]}=\${1:${part[3]}}`
);
items.push(item);
}
if (items.length > 0) return items;
}
}
23 changes: 23 additions & 0 deletions src/extension/providers/completion/named-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CompletionItem, CompletionItemKind, TextDocument } from "vscode";
import { getNginxConfDefinitionInfo } from "../../parser";

const _directives = new Set<string>(["error_page", "try_files"]);
export function _doesDirectiveCanUseNamedLocation(directive: string) {
return _directives.has(directive);
}

export function _completeNameLocation(document: TextDocument, input: string) {
const { location } = getNginxConfDefinitionInfo(document.getText());
const locationNames = Array.from(new Set(location.map((it) => it.name)));
if (locationNames.length < 1) return;

input = input.toLowerCase();
return locationNames
.map((it) => {
if (input && !it.toLowerCase().startsWith(input)) return null;
const ci = new CompletionItem(it, CompletionItemKind.Reference);
ci.insertText = it.slice(1);
return ci;
})
.filter((it) => it);
}
Loading

0 comments on commit 77c5d39

Please sign in to comment.