Skip to content

Commit 9511d78

Browse files
committed
feat: add goto symbol support
1 parent 59ab9da commit 9511d78

File tree

5 files changed

+263
-5
lines changed

5 files changed

+263
-5
lines changed

src/definitions.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { type Disposable, Location, languages, type Position, type TextDocument, Uri, workspace } from 'vscode';
2+
3+
const NSIS_GLOB = '**/*.{nsi,nsh,bnsi,bnsh,nsdinc}';
4+
5+
const DEFINITION_PATTERNS = {
6+
function: /^\s*Function\s+(\.?\w+)/i,
7+
macro: /^\s*!macro\s+(\w+)/i,
8+
define: /^\s*!define\s+(?:\/\w+\s+)*(\w+)/i,
9+
variable: /^\s*Var\s+(?:\/GLOBAL\s+)?"?(\w+)"?/i,
10+
};
11+
12+
function getWordAtPosition(document: TextDocument, position: Position): string | undefined {
13+
const range = document.getWordRangeAtPosition(position, /[\w.]+/);
14+
return range ? document.getText(range) : undefined;
15+
}
16+
17+
async function findDefinitions(name: string): Promise<Location[]> {
18+
const locations: Location[] = [];
19+
const files = await workspace.findFiles(NSIS_GLOB);
20+
21+
for (const file of files) {
22+
const document = await workspace.openTextDocument(file);
23+
24+
for (let i = 0; i < document.lineCount; i++) {
25+
const line = document.lineAt(i);
26+
27+
for (const pattern of Object.values(DEFINITION_PATTERNS)) {
28+
const match = pattern.exec(line.text);
29+
30+
if (match?.[1] === name) {
31+
const nameStart = line.text.indexOf(name, match.index);
32+
locations.push(
33+
new Location(
34+
document.uri,
35+
document.positionAt(document.offsetAt(document.lineAt(i).range.start) + nameStart),
36+
),
37+
);
38+
break;
39+
}
40+
}
41+
}
42+
}
43+
44+
return locations;
45+
}
46+
47+
export function registerDefinitionProvider(): Disposable {
48+
return languages.registerDefinitionProvider('nsis', {
49+
async provideDefinition(document, position) {
50+
const word = getWordAtPosition(document, position);
51+
52+
if (!word) {
53+
return [];
54+
}
55+
56+
return findDefinitions(word);
57+
},
58+
});
59+
}

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export async function activate(context: ExtensionContext): Promise<void> {
4949
context.subscriptions.push(formatterDisposable);
5050
}
5151

52+
// Symbol Navigation
53+
const { registerDefinitionProvider } = await import('./definitions.ts');
54+
const { registerReferenceProvider } = await import('./references.ts');
55+
const { registerSymbolProvider } = await import('./symbols.ts');
56+
57+
context.subscriptions.push(registerDefinitionProvider(), registerReferenceProvider(), registerSymbolProvider());
58+
5259
// Diagnostics
5360
const { updateDiagnostics } = await import('./diagnostics.ts');
5461
const collection = languages.createDiagnosticCollection('nsis');

src/references.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { type Disposable, Location, languages, type Position, type TextDocument, workspace } from 'vscode';
2+
3+
const NSIS_GLOB = '**/*.{nsi,nsh,bnsi,bnsh,nsdinc}';
4+
5+
const DEFINITION_PATTERNS = {
6+
function: /^\s*Function\s+(\.?\w+)/i,
7+
macro: /^\s*!macro\s+(\w+)/i,
8+
define: /^\s*!define\s+(?:\/\w+\s+)*(\w+)/i,
9+
variable: /^\s*Var\s+(?:\/GLOBAL\s+)?"?(\w+)"?/i,
10+
};
11+
12+
function getWordAtPosition(document: TextDocument, position: Position): string | undefined {
13+
const range = document.getWordRangeAtPosition(position, /[\w.]+/);
14+
return range ? document.getText(range) : undefined;
15+
}
16+
17+
function getReferencePatterns(name: string): RegExp[] {
18+
const escaped = name.replace(/\./g, '\\.');
19+
20+
return [
21+
// Function references: Call name, GetFunctionAddress ... name
22+
new RegExp(`\\b(?:Call|GetFunctionAddress\\s+\\S+)\\s+${escaped}\\b`, 'i'),
23+
// Macro references: !insertmacro name
24+
new RegExp(`^\\s*!insertmacro\\s+${escaped}\\b`, 'i'),
25+
// Define references: ${name}
26+
new RegExp(`\\$\\{${escaped}\\}`),
27+
// Variable references: $name
28+
new RegExp(`\\$${escaped}\\b`),
29+
];
30+
}
31+
32+
function isDefinitionLine(line: string, name: string): boolean {
33+
for (const pattern of Object.values(DEFINITION_PATTERNS)) {
34+
const match = pattern.exec(line);
35+
36+
if (match?.[1] === name) {
37+
return true;
38+
}
39+
}
40+
41+
return false;
42+
}
43+
44+
export function registerReferenceProvider(): Disposable {
45+
return languages.registerReferenceProvider('nsis', {
46+
async provideReferences(document, position, context) {
47+
const word = getWordAtPosition(document, position);
48+
49+
if (!word) {
50+
return [];
51+
}
52+
53+
const locations: Location[] = [];
54+
const patterns = getReferencePatterns(word);
55+
const files = await workspace.findFiles(NSIS_GLOB);
56+
57+
for (const file of files) {
58+
const doc = await workspace.openTextDocument(file);
59+
60+
for (let i = 0; i < doc.lineCount; i++) {
61+
const line = doc.lineAt(i);
62+
63+
if (!context.includeDeclaration && isDefinitionLine(line.text, word)) {
64+
continue;
65+
}
66+
67+
for (const pattern of patterns) {
68+
const match = pattern.exec(line.text);
69+
70+
if (match) {
71+
const nameStart = line.text.indexOf(word, match.index);
72+
73+
if (nameStart !== -1) {
74+
locations.push(new Location(doc.uri, doc.positionAt(doc.offsetAt(line.range.start) + nameStart)));
75+
}
76+
77+
break;
78+
}
79+
}
80+
}
81+
}
82+
83+
return locations;
84+
},
85+
});
86+
}

src/symbols.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { type Disposable, DocumentSymbol, languages, type Position, Range, SymbolKind } from 'vscode';
2+
3+
const DEFINITION_PATTERNS = [
4+
{ pattern: /^\s*Function\s+(\.?\w+)/i, kind: SymbolKind.Function },
5+
{ pattern: /^\s*!macro\s+(\w+)/i, kind: SymbolKind.Function },
6+
{ pattern: /^\s*!define\s+(?:\/\w+\s+)*(\w+)/i, kind: SymbolKind.Constant },
7+
{ pattern: /^\s*Var\s+(?:\/GLOBAL\s+)?"?(\w+)"?/i, kind: SymbolKind.Variable },
8+
];
9+
10+
export function registerSymbolProvider(): Disposable {
11+
return languages.registerDocumentSymbolProvider('nsis', {
12+
provideDocumentSymbols(document) {
13+
const symbols: DocumentSymbol[] = [];
14+
15+
for (let i = 0; i < document.lineCount; i++) {
16+
const line = document.lineAt(i);
17+
18+
for (const { pattern, kind } of DEFINITION_PATTERNS) {
19+
const match = pattern.exec(line.text);
20+
21+
if (match?.[1]) {
22+
const name = match[1];
23+
const nameStart = line.text.indexOf(name, match.index);
24+
const nameRange = new Range(i, nameStart, i, nameStart + name.length);
25+
26+
symbols.push(new DocumentSymbol(name, '', kind, line.range, nameRange));
27+
break;
28+
}
29+
}
30+
}
31+
32+
return symbols;
33+
},
34+
});
35+
}

syntaxes/nsis.tmLanguage

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<key>comment</key>
6-
<string>
7-
todo: - highlight functions
8-
</string>
9-
105
<key>fileTypes</key>
116
<array>
127
<string>nsi</string>
@@ -20,6 +15,82 @@
2015
<string>NSIS</string>
2116
<key>patterns</key>
2217
<array>
18+
<dict>
19+
<key>comment</key>
20+
<string>Function definition</string>
21+
<key>match</key>
22+
<string>^\s*(?i)(Function)\s+(\.?\w+)</string>
23+
<key>captures</key>
24+
<dict>
25+
<key>1</key>
26+
<dict>
27+
<key>name</key>
28+
<string>keyword.nsis</string>
29+
</dict>
30+
<key>2</key>
31+
<dict>
32+
<key>name</key>
33+
<string>entity.name.function.nsis</string>
34+
</dict>
35+
</dict>
36+
</dict>
37+
<dict>
38+
<key>comment</key>
39+
<string>Macro definition</string>
40+
<key>match</key>
41+
<string>^\s*(?i)(\!macro)\s+(\w+)</string>
42+
<key>captures</key>
43+
<dict>
44+
<key>1</key>
45+
<dict>
46+
<key>name</key>
47+
<string>keyword.other.nsis</string>
48+
</dict>
49+
<key>2</key>
50+
<dict>
51+
<key>name</key>
52+
<string>entity.name.function.macro.nsis</string>
53+
</dict>
54+
</dict>
55+
</dict>
56+
<dict>
57+
<key>comment</key>
58+
<string>Define definition</string>
59+
<key>match</key>
60+
<string>^\s*(?i)(\!define)\s+(?:/\w+\s+)*(\w+)</string>
61+
<key>captures</key>
62+
<dict>
63+
<key>1</key>
64+
<dict>
65+
<key>name</key>
66+
<string>keyword.other.nsis</string>
67+
</dict>
68+
<key>2</key>
69+
<dict>
70+
<key>name</key>
71+
<string>entity.name.constant.define.nsis</string>
72+
</dict>
73+
</dict>
74+
</dict>
75+
<dict>
76+
<key>comment</key>
77+
<string>Variable declaration</string>
78+
<key>match</key>
79+
<string>^\s*(?i)(Var)\s+(?:/GLOBAL\s+)?"?(\w+)"?</string>
80+
<key>captures</key>
81+
<dict>
82+
<key>1</key>
83+
<dict>
84+
<key>name</key>
85+
<string>keyword.nsis</string>
86+
</dict>
87+
<key>2</key>
88+
<dict>
89+
<key>name</key>
90+
<string>entity.name.variable.nsis</string>
91+
</dict>
92+
</dict>
93+
</dict>
2394
<dict>
2495
<key>match</key>
2596
<string>^\s*(?i)(Abort|AddBrandingImage|AddSize|AllowRootDirInstall|AllowSkipFiles|AutoCloseWindow|BGFont|BGGradient|BrandingText|BringToFront|Call|CallInstDLL|Caption|ChangeUI|CheckBitmap|ClearErrors|CompletedText|ComponentText|CopyFiles|CPU|CRCCheck|CreateDirectory|CreateFont|CreateShortCut|Delete|DeleteINISec|DeleteINIStr|DeleteRegKey|DeleteRegValue|DetailPrint|DetailsButtonText|DirText|DirVar|DirVerify|EnableWindow|EnumRegKey|EnumRegValue|Exch|Exec|ExecShell|ExecShellWait|ExecWait|ExpandEnvStrings|File|FileBufSize|FileClose|FileErrorText|FileOpen|FileRead|FileReadByte|FileReadUTF16LE|FileReadWord|FileWriteUTF16LE|FileSeek|FileWrite|FileWriteByte|FileWriteWord|FindClose|FindFirst|FindNext|FindWindow|FlushINI|Function(End)?|GetCurInstType|GetCurrentAddress|GetDlgItem|GetDLLVersion|GetDLLVersionLocal|GetErrorLevel|GetFileTime|GetFileTimeLocal|GetFullPathName|GetFunctionAddress|GetInstDirError|GetKnownFolderPath|GetLabelAddress|GetRegView|GetShellVarContext|GetTempFileName|GetWinVer|Goto|HideWindow|Icon|IfAbort|IfAltRegView|IfErrors|IfFileExists|IfRebootFlag|IfRtlLanguage|IfShellVarContextAll|IfSilent|InitPluginsDir|InstallButtonText|InstallColors|InstallDir|InstallDirRegKey|InstProgressFlags|InstType|InstTypeGetText|InstTypeSetText|Int64Cmp|Int64CmpU|Int64Fmt|IntCmp|IntCmpU|IntFmt|IntOp|IntPtrCmp|IntPtrCmpU|IntPtrOp|IsWindow|LangString|LicenseBkColor|LicenseData|LicenseForceSelection|LicenseLangString|LicenseText|LoadAndSetImage|LoadLanguageFile|LockWindow|LogSet|LogText|ManifestAppendCustomString|ManifestDisableWindowFiltering|ManifestDPIAware|ManifestGdiScaling|ManifestLongPathAware|ManifestMaxVersionTested|ManifestSupportedOS|MessageBox|MiscButtonText|Name|Nop|OutFile|Page|PageCallbacks|PageEx(End)?|PEAddResource|PEDllCharacteristics|PERemoveResource|PESubsysVer|Pop|Push|Quit|ReadEnvStr|ReadINIStr|ReadMemory|ReadRegDWORD|ReadRegStr|Reboot|RegDLL|Rename|RequestExecutionLevel|ReserveFile|Return|RMDir|SearchPath|Section(End)?|SectionGroup(End)?|SectionGetFlags|SectionGetInstTypes|SectionGetSize|SectionGetText|SectionIn|SectionSetFlags|SectionSetInstTypes|SectionSetSize|SectionSetText|SendMessage|SetAutoClose|SetBrandingImage|SetCompress|SetCompressionLevel|SetCompressor|SetCompressorDictSize|SetCtlColors|SetCurInstType|SetDatablockOptimize|SetDateSave|SetDetailsPrint|SetDetailsView|SetErrorLevel|SetErrors|SetFileAttributes|SetFont|SetOutPath|SetOverwrite|SetRebootFlag|SetRegView|SetShellVarContext|SetSilent|ShowInstDetails|ShowUninstDetails|ShowWindow|SilentInstall|SilentUnInstall|Sleep|SpaceTexts|StrCmp|StrCmpS|StrCpy|StrLen|SubCaption|Target|Unicode|UninstallButtonText|UninstallCaption|UninstallIcon|UninstallSubCaption|UninstallText|UninstPage|UnRegDLL|UnsafeStrCpy|Var|VIAddVersionKey|VIFileVersion|VIProductVersion|WindowIcon|WriteINIStr|WriteRegBin|WriteRegDWORD|WriteRegExpandStr|WriteRegMultiStr|WriteRegNone|WriteRegStr|WriteUninstaller|XPStyle)\b</string>

0 commit comments

Comments
 (0)