Skip to content
Open
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
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@
}
}
},
"nwscript-ee-lsp.definition": {
"type": "object",
"properties": {
"preferImplementation": {
"type": "boolean",
"default": true,
"description": "When using Go to Definition on a function, prefer jumping to the implementation (with body) over the forward declaration. When disabled, jumps to the first occurrence found."
}
}
},
"nwscript-ee-lsp.compiler": {
"type": "object",
"properties": {
Expand Down
Binary file modified server/resources/compiler/linux/nwn_script_comp
Binary file not shown.
Binary file modified server/resources/compiler/mac/nwn_script_comp
Binary file not shown.
Binary file modified server/resources/compiler/windows/nwn_script_comp.exe
Binary file not shown.
60 changes: 42 additions & 18 deletions server/src/Providers/DiagnosticsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,27 @@ export default class DiagnoticsProvider extends Provider {

private generateDiagnostics(uris: string[], files: FilesDiagnostics, severity: DiagnosticSeverity) {
return (line: string) => {
const uri = uris.find((uri) => basename(fileURLToPath(uri)) === lineFilename.exec(line)![2]);

if (uri) {
const linePosition = Number(lineNumber.exec(line)![1]) - 1;
const diagnostic = {
severity,
range: {
start: { line: linePosition, character: 0 },
end: { line: linePosition, character: Number.MAX_VALUE },
},
message: lineMessage.exec(line)![2].trim(),
};

files[uri].push(diagnostic);
const lineFilenameMatch = lineFilename.exec(line);
if (lineFilenameMatch) {
const reportedFileName = lineFilenameMatch[2];
const uri = uris.find((uri) => basename(fileURLToPath(uri)) === reportedFileName);
if (uri) {
const lineNumberMatch = lineNumber.exec(line);
const lineMessageMatch = lineMessage.exec(line);
if (lineNumberMatch && lineMessageMatch) {
const linePosition = Number(lineNumberMatch[1]) - 1;
const diagnostic = {
severity,
range: {
start: { line: linePosition, character: 0 },
end: { line: linePosition, character: Number.MAX_VALUE },
},
message: lineMessageMatch[2].trim(),
};

files[uri].push(diagnostic);
}
}
}
};
}
Expand Down Expand Up @@ -101,7 +108,10 @@ export default class DiagnoticsProvider extends Provider {
// The compiler command:
// - y; continue on error
// - s; dry run
const args = ["-y", "-s"];
// - n; no entry point required (for include files)
// - E; collect and report all errors (not just the first)
// Note: -E requires compiler binary with ABI v2+
const args = ["-y", "-s", "-n", "-E"];
if (Boolean(nwnHome)) {
args.push("--userdirectory");
args.push(`"${nwnHome}"`);
Expand All @@ -114,12 +124,25 @@ export default class DiagnoticsProvider extends Provider {
} else if (verbose) {
this.server.logger.info("Trying to resolve Neverwinter Nights installation directory automatically.");
}
if (children.length > 0) {
// Collect directories from ALL indexed documents so the compiler can
// resolve the full transitive include chain, not just direct children.
const allDirs: Set<string> = new Set();
this.server.documentsCollection.forEach((doc) => {
if (doc.uri && doc.uri.startsWith("file://")) {
allDirs.add(dirname(fileURLToPath(doc.uri)));
}
});
// Also add the compiled file's own directory
allDirs.add(dirname(fileURLToPath(uri)));
if (allDirs.size > 0) {
args.push("--dirs");
args.push(`"${[...new Set(uris.map((uri) => dirname(fileURLToPath(uri))))].join(",")}"`);
args.push(`"${[...allDirs].join(",")}"`);
}

const filePath = fileURLToPath(uri);
this.server.logger.info(`Compiling file: ${filePath}`);
args.push("-c");
args.push(`"${fileURLToPath(uri)}"`);
args.push(`"${filePath}"`);

let stdout = "";
let stderr = "";
Expand Down Expand Up @@ -186,6 +209,7 @@ export default class DiagnoticsProvider extends Provider {
for (const [uri, diagnostics] of Object.entries(files)) {
this.server.connection.sendDiagnostics({ uri, diagnostics });
}

resolve(true);
});
});
Expand Down
43 changes: 37 additions & 6 deletions server/src/Providers/GotoDefinitionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TextDocument } from "vscode-languageserver-textdocument";

import type { OwnedComplexTokens, OwnedStructComplexTokens } from "../Documents/Document";
import type { ServerManager } from "../ServerManager";
import type { ComplexToken } from "../Tokenizer/types";
import type { ComplexToken, FunctionComplexToken } from "../Tokenizer/types";
import { Document } from "../Documents";
import Provider from "./Provider";

Expand Down Expand Up @@ -71,12 +71,43 @@ export default class GotoDefinitionProvider extends Provider {
tokensWithRef.push({ owner: localStandardLibDefinitions?.uri, tokens: localStandardLibDefinitions?.complexTokens });
}

loop: for (let i = 0; i < tokensWithRef.length; i++) {
ref = tokensWithRef[i];
if (this.server.config.definition.preferImplementation) {
// Two-pass: prefer function definitions (with bodies) over forward declarations
let fallbackToken: ComplexToken | undefined;
let fallbackRef: OwnedComplexTokens | undefined;

loop: for (let i = 0; i < tokensWithRef.length; i++) {
ref = tokensWithRef[i];

for (const candidate of ref?.tokens ?? []) {
if (candidate.identifier !== rawContent) continue;

const funcCandidate = candidate as FunctionComplexToken;
if (funcCandidate.tokenType === CompletionItemKind.Function && funcCandidate.isForwardDeclaration) {
if (!fallbackToken) {
fallbackToken = candidate;
fallbackRef = ref;
}
} else {
token = candidate;
break loop;
}
}
}

token = ref?.tokens.find((candidate) => candidate.identifier === rawContent);
if (token) {
break loop;
if (!token && fallbackToken) {
token = fallbackToken;
ref = fallbackRef;
}
} else {
// Jump to first occurrence found
loop: for (let i = 0; i < tokensWithRef.length; i++) {
ref = tokensWithRef[i];

token = ref?.tokens.find((candidate) => candidate.identifier === rawContent);
if (token) {
break loop;
}
}
}
break;
Expand Down
3 changes: 3 additions & 0 deletions server/src/ServerManager/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const defaultServerConfiguration = {
nwnHome: "",
nwnInstallation: "",
},
definition: {
preferImplementation: true,
},
};
/* eslint-enable @typescript-eslint/naming-convention */

Expand Down
3 changes: 2 additions & 1 deletion server/src/ServerManager/ServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,12 @@ export default class ServerManger {
}

private async loadConfig() {
const { completion, hovering, formatter, compiler, ...rest } = await this.connection.workspace.getConfiguration("nwscript-ee-lsp");
const { completion, hovering, formatter, compiler, definition, ...rest } = await this.connection.workspace.getConfiguration("nwscript-ee-lsp");
this.config = { ...this.config, ...rest };
this.config.completion = { ...this.config.completion, ...completion };
this.config.hovering = { ...this.config.hovering, ...hovering };
this.config.formatter = { ...this.config.formatter, ...formatter };
this.config.compiler = { ...this.config.compiler, ...compiler };
this.config.definition = { ...this.config.definition, ...definition };
}
}
9 changes: 5 additions & 4 deletions server/src/Tokenizer/Tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,11 @@ export default class Tokenizer {
return isFunctionDeclaration;
}

private isGlobalFunctionDeclaration(lineIndex: number, tokenIndex: number, token: IToken, tokensArrays: (IToken[] | undefined)[]) {
private isGlobalFunctionDeclarationOrDefinition(lineIndex: number, tokenIndex: number, token: IToken, tokensArrays: (IToken[] | undefined)[]) {
return (
!(tokenIndex === 0 && lineIndex === 0) && // Not sure why we need this
!token.scopes.includes(LanguageScopes.block) &&
token.scopes.includes(LanguageScopes.functionIdentifier) &&
this.isFunctionDeclaration(lineIndex, tokensArrays)
token.scopes.includes(LanguageScopes.functionIdentifier)
);
}

Expand Down Expand Up @@ -256,14 +255,16 @@ export default class Tokenizer {
break;
}

if (this.isGlobalFunctionDeclaration(lineIndex, tokenIndex, token, tokensArrays)) {
if (this.isGlobalFunctionDeclarationOrDefinition(lineIndex, tokenIndex, token, tokensArrays)) {
const isForwardDeclaration = this.isFunctionDeclaration(lineIndex, tokensArrays);
scope.complexTokens.push({
position: { line: lineIndex, character: token.startIndex },
identifier: this.getRawTokenContent(line, token),
tokenType: CompletionItemKind.Function,
returnType: tokenIndex === 0 ? this.getTokenLanguageType(lines[lineIndex - 1], tokensArrays[lineIndex - 1]!, 0) : this.getTokenLanguageType(line, tokensArray, tokenIndex - 2),
params: this.getFunctionParams(lineIndex, lines, tokensArrays),
comments: this.getFunctionComments(lines, tokensArrays, tokenIndex === 0 ? lineIndex - 2 : lineIndex - 1),
isForwardDeclaration,
});

break;
Expand Down
1 change: 1 addition & 0 deletions server/src/Tokenizer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type LanguageFunction = {
params: FunctionParamComplexToken[];
variables?: VariableComplexToken[];
comments: string[];
isForwardDeclaration?: boolean;
};
type LanguageFunctionParam = {
tokenType: typeof CompletionItemKind.TypeParameter;
Expand Down
Loading