Skip to content

Commit 372545d

Browse files
committed
feat: search NodeTags in files when debug starts
Previously this was launched on start and used ineffective algorithm with lots of RegExp which allocates too much memory. Now this part is reworked: - use simple one-pass algorithm based on string search (indexOf) - search is launched on debug session starts - files for parsing searched based on detected PG version (modern requires only nodetags.h)
1 parent 80a107d commit 372545d

File tree

3 files changed

+297
-5
lines changed

3 files changed

+297
-5
lines changed

src/configuration.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as vscode from 'vscode';
33
import * as vars from './variables';
44
import * as utils from './utils';
55
import { Log as logger } from './logger';
6+
import { PghhError } from './error';
67

78
export interface VariablesConfiguration {
89
/* Array special members */
@@ -59,6 +60,13 @@ export function parseFormatterConfiguration(configFile: unknown): PgindentConfig
5960

6061
export function parseVariablesConfiguration(configFile: unknown): VariablesConfiguration | undefined {
6162
const parseArrayMember = (obj: unknown): vars.ArraySpecialMemberInfo | undefined => {
63+
/*
64+
* {
65+
* "typeName": "parentType",
66+
* "memberName": "memberOfParent",
67+
* "lengthExpr": "expression to get length"
68+
* }
69+
*/
6270
if (!(typeof obj === 'object' && obj)) {
6371
return;
6472
}
@@ -135,6 +143,12 @@ export function parseVariablesConfiguration(configFile: unknown): VariablesConfi
135143
};
136144

137145
const parseSingleAlias = (obj: unknown): vars.AliasInfo | undefined => {
146+
/*
147+
* {
148+
* "alias": "name of alias",
149+
* "type": "real type"
150+
* }
151+
*/
138152
if (!(typeof obj === 'object' && obj)) {
139153
return;
140154
}
@@ -597,7 +611,6 @@ export async function getFormatterConfiguration() {
597611

598612
export async function refreshConfiguration() {
599613
/* Do not check 'dirtyFlag', because this function must be invoked explicitly */
600-
601614
if (!vscode.workspace.workspaceFolders?.length) {
602615
return;
603616
}
@@ -625,6 +638,56 @@ export async function refreshConfiguration() {
625638
configDirty = false;
626639
}
627640

641+
async function findConfigurationFile() {
642+
if (!vscode.workspace.workspaceFolders?.length) {
643+
throw new PghhError('No workspace folders opened');
644+
}
645+
646+
for (const folder of vscode.workspace.workspaceFolders) {
647+
const file = getExtensionConfigFile(folder.uri);
648+
if (await utils.fileExists(file)) {
649+
return file;
650+
}
651+
}
652+
653+
const file = getExtensionConfigFile(vscode.workspace.workspaceFolders[0].uri);
654+
const directory = utils.joinPath(file, '..');
655+
if (!await utils.directoryExists(directory)) {
656+
logger.info('.vscode directory does not exist - creating one at %s', directory.fsPath);
657+
await utils.createDirectory(directory);
658+
}
659+
660+
/* It will be created if necessary */
661+
return file;
662+
}
663+
664+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
665+
export async function mutateConfiguration(mutator: (config: any) => unknown) {
666+
const configFile = await findConfigurationFile();
667+
668+
let contents: string | undefined;
669+
try {
670+
contents = await utils.readFile(configFile);
671+
} catch (err) {
672+
if (!(err instanceof Error && err.name.indexOf('EntryNotFound') !== -1)) {
673+
throw err;
674+
}
675+
contents = undefined;
676+
}
677+
678+
let config;
679+
if (contents?.length) {
680+
config = JSON.parse(contents);
681+
} else {
682+
config = undefined;
683+
}
684+
685+
config = mutator(config);
686+
687+
await utils.writeFile(configFile, JSON.stringify(config, undefined, ' '));
688+
configDirty = true;
689+
}
690+
628691
export function markConfigFileDirty() {
629692
configDirty = true;
630693
}

src/extension.ts

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
import * as path from 'path';
12
import * as vscode from 'vscode';
3+
24
import * as utils from './utils';
35
import { Features } from './utils';
46
import * as vars from './variables';
57
import * as dbg from './debugger';
68
import * as dap from './dap';
7-
import { Commands, ExtensionSettingsFileName, PgVariablesViewName, getExtensionConfigFile, markConfigFileDirty, refreshConfiguration, setupVsCodeSettings } from './configuration';
9+
import { Commands,
10+
ExtensionSettingsFileName,
11+
PgVariablesViewName,
12+
VsCodeSettings,
13+
getExtensionConfigFile,
14+
markConfigFileDirty,
15+
mutateConfiguration,
16+
refreshConfiguration,
17+
setupVsCodeSettings } from './configuration';
818
import { Log as logger } from './logger';
919
import { setupPgConfSupport } from './pgconf';
1020
import { setupFormatting } from './formatter';
@@ -511,9 +521,163 @@ function setupPgVariablesView(context: vscode.ExtensionContext) {
511521
/* Setup debugger specific function */
512522
dbg.setupDebugger(context, pgvars);
513523

524+
/*
525+
* On start try to detect new NodeTags and if they exists, add to NodeVars
526+
* and ask user to add them to configuration file.
527+
*/
528+
const disposable = pgvars.onDidDebugStart(async (c) => {
529+
/* Run this only once */
530+
disposable.dispose();
531+
532+
try {
533+
await searchNodeTagsWorker(c);
534+
} catch (err) {
535+
logger.error('could not search for new NodeTags', err);
536+
}
537+
});
538+
539+
514540
return pgvars;
515541
}
516542

543+
async function findAllFilesWithNodeTags(folders: readonly vscode.WorkspaceFolder[],
544+
pgversion: number) {
545+
const paths = [];
546+
for (const folder of folders) {
547+
/*
548+
* Starting from 16 major version NodeTag is autogenerated and stored in nodetags.h.
549+
* Avoid parsing 'nodes.h', because it will not give us anything.
550+
*/
551+
let file;
552+
if (16_00_00 <= pgversion) {
553+
file = utils.getWorkspacePgSrcFile(folder.uri, 'src', 'include', 'nodes', 'nodetags.h');
554+
} else {
555+
file = utils.getWorkspacePgSrcFile(folder.uri, 'src', 'include', 'nodes', 'nodes.h');
556+
}
557+
558+
if (await utils.fileExists(file)) {
559+
paths.push(file);
560+
}
561+
}
562+
563+
const customFiles = VsCodeSettings.getCustomNodeTagFiles();
564+
if (!customFiles) {
565+
return paths;
566+
}
567+
568+
/* Search custom provided NodeTag files */
569+
for (const customFile of customFiles) {
570+
let uri;
571+
if (path.isAbsolute(customFile)) {
572+
uri = vscode.Uri.file(customFile);
573+
if (!await utils.fileExists(uri)) {
574+
continue;
575+
}
576+
} else {
577+
for (const folder of folders) {
578+
uri = utils.getWorkspacePgSrcFile(folder.uri, customFile);
579+
if (await utils.fileExists(uri)) {
580+
break;
581+
}
582+
}
583+
if (!uri) {
584+
continue;
585+
}
586+
}
587+
588+
paths.push(uri);
589+
}
590+
591+
return paths;
592+
}
593+
594+
async function addNodeTagsToConfiguration(nodetags: Set<string>) {
595+
logger.info('adding new NodeTags to configuration file');
596+
try {
597+
await mutateConfiguration((config) => {
598+
/*
599+
* Configuration can not exist or it can contain some garbage.
600+
* For now I don't know how to perfectly handle this, so
601+
* just replace entire field if it's not an array.
602+
*/
603+
if (config === undefined || config === null) {
604+
return {
605+
nodetags: Array.from(nodetags),
606+
};
607+
} else if (typeof config === 'object') {
608+
if ('nodetags' in config && Array.isArray(config.nodetags)) {
609+
config.nodetags.push(...Array.from(nodetags));
610+
} else {
611+
config.nodetags = Array.from(nodetags);
612+
}
613+
}
614+
return config;
615+
});
616+
} catch (err) {
617+
logger.error('could not add nodetags to configuration file', err);
618+
}
619+
}
620+
621+
/*
622+
* Run Worker that will traverse all NodeTag containing files,
623+
* parse NodeTags and find which are missing - if find something,
624+
* then user is prompted to add them to configuration file.
625+
*
626+
* This is quiet CPU intensive operation, so perform in another thread.
627+
*/
628+
async function searchNodeTagsWorker(context: vars.ExecContext) {
629+
if (!vscode.workspace.workspaceFolders?.length) {
630+
return;
631+
}
632+
633+
if (!context.pgversion) {
634+
return;
635+
}
636+
637+
/* Find all files containing NodeTags */
638+
const paths = await findAllFilesWithNodeTags(
639+
vscode.workspace.workspaceFolders, context.pgversion);
640+
if (!paths.length) {
641+
logger.debug('no NodeTag files found');
642+
return;
643+
}
644+
645+
/* Run worker and collect result */
646+
const newNodeTags = new Set<string>();
647+
for (const path of paths) {
648+
const tags = await vars.parseNodeTagsFile(path);
649+
if (tags?.size) {
650+
for (const t of tags) {
651+
newNodeTags.add(t);
652+
}
653+
}
654+
}
655+
656+
for (const tag of context.nodeVarRegistry.nodeTags) {
657+
newNodeTags.delete(tag);
658+
}
659+
660+
if (!newNodeTags.size) {
661+
/* No new NodeTags found */
662+
return;
663+
}
664+
665+
for (const tag of newNodeTags) {
666+
context.nodeVarRegistry.nodeTags.add(tag);
667+
}
668+
669+
logger.info('found %i new node tags', newNodeTags.size);
670+
const answer = await vscode.window.showInformationMessage(
671+
`Found ${newNodeTags.size} new NodeTags. ` +
672+
`Would you like to add them to configuration file?`,
673+
'Yes', 'No');
674+
if (answer !== 'Yes') {
675+
return;
676+
}
677+
678+
await addNodeTagsToConfiguration(newNodeTags);
679+
}
680+
517681
function setupConfiguration(context: vscode.ExtensionContext) {
518682
/* Mark configuration dirty when user changes it - no eager parsing */
519683
const registerFolderWatcher = (folder: vscode.WorkspaceFolder) => {
@@ -526,7 +690,7 @@ function setupConfiguration(context: vscode.ExtensionContext) {
526690
configFileWatcher.onDidCreate(markConfigFileDirty, undefined, context.subscriptions);
527691
configFileWatcher.onDidDelete(markConfigFileDirty, undefined, context.subscriptions);
528692
};
529-
693+
530694
if (vscode.workspace.workspaceFolders?.length) {
531695
vscode.workspace.workspaceFolders.forEach(registerFolderWatcher);
532696
} else {

src/variables.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,11 @@ export class StepContext {
436436
* Context of current execution.
437437
*/
438438
export class ExecContext {
439+
/*
440+
* Version number of debugging PG instance
441+
*/
442+
pgversion?: number;
443+
439444
/**
440445
* Registry about NodeTag variables information
441446
*/
@@ -5404,7 +5409,14 @@ export class PgVariablesViewProvider implements vscode.TreeDataProvider<Variable
54045409
this.context?.step.reset();
54055410
this._onDidChangeTreeData.fire();
54065411
}
5407-
5412+
5413+
private _onDidDebugStart = new vscode.EventEmitter<ExecContext>();
5414+
/*
5415+
* Emitted when debug session started and we return data.
5416+
* Passes obtained PG version.
5417+
*/
5418+
readonly onDidDebugStart = this._onDidDebugStart.event;
5419+
54085420
startDebugging(debug: dbg.GenericDebuggerFacade) {
54095421
this.debug?.dispose();
54105422
this.debug = debug;
@@ -5590,7 +5602,12 @@ export class PgVariablesViewProvider implements vscode.TreeDataProvider<Variable
55905602
}
55915603

55925604
const context = new ExecContext(this.getDebug(), nodeVars, specialMembers, hashTables);
5593-
5605+
context.pgversion = pgversion;
5606+
5607+
if (pgversion) {
5608+
this._onDidDebugStart.fire(context);
5609+
}
5610+
55945611
return context;
55955612
}
55965613

@@ -5664,3 +5681,51 @@ export class PgVariablesViewProvider implements vscode.TreeDataProvider<Variable
56645681
this._onDidChangeTreeData.dispose();
56655682
}
56665683
}
5684+
5685+
function isSpace(char: string) {
5686+
return char === ' ' || char === '\t' || char === '\n';
5687+
}
5688+
5689+
function isIdentifierChar(char: string) {
5690+
return ('a' <= char && char <= 'z') || ('A' <= char && char <= 'Z')
5691+
|| char === '_' /* put it here, because underscore is more common, than digit */
5692+
|| ('0' <= char && char <= '9');
5693+
}
5694+
5695+
export async function parseNodeTagsFile(file: vscode.Uri) {
5696+
let content;
5697+
try {
5698+
const document = await vscode.workspace.openTextDocument(file);
5699+
content = document.getText();
5700+
} catch (error) {
5701+
logger.error('could not open NodeTags file %s', file.fsPath, error);
5702+
return;
5703+
}
5704+
5705+
const nodeTags: string[] = [];
5706+
let prefixIndex = undefined;
5707+
while ((prefixIndex = content.indexOf('T_', prefixIndex)) !== -1) {
5708+
/* Check this is start of identifier (not false positive) */
5709+
if (prefixIndex > 0 && !isSpace(content[prefixIndex - 1])) {
5710+
prefixIndex += 2;
5711+
continue;
5712+
}
5713+
5714+
/* Search for end of identifier */
5715+
let endOfIdent = prefixIndex + 2;
5716+
while (endOfIdent < content.length && isIdentifierChar(content[endOfIdent])) {
5717+
endOfIdent++;
5718+
}
5719+
5720+
/* End of file - should not happen */
5721+
if (content.length <= endOfIdent) {
5722+
break;
5723+
}
5724+
5725+
const tag = content.substring(prefixIndex + 2, endOfIdent);
5726+
nodeTags.push(tag);
5727+
prefixIndex = endOfIdent + 1;
5728+
}
5729+
5730+
return new Set(nodeTags);
5731+
}

0 commit comments

Comments
 (0)