From f953d37cebc43eec186d30bc51191fad5645802c Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Tue, 26 Aug 2025 20:49:48 -0400 Subject: [PATCH 01/11] Add new configuration and refresh button Signed-off-by: Sanjula Ganepola --- package-lock.json | 4 +- package.json | 38 ++++++++-- src/views/examples/contributes.json | 110 +++++++++++++++++----------- 3 files changed, 103 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f252276..4df72b0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-db2i", - "version": "1.13.3", + "version": "1.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-db2i", - "version": "1.13.3", + "version": "1.14.0", "dependencies": { "@ibm/mapepire-js": "^0.5.0", "@octokit/rest": "^21.1.1", diff --git a/package.json b/package.json index 6acf73f2..b7cfdf59 100644 --- a/package.json +++ b/package.json @@ -255,6 +255,21 @@ } } }, + { + "id": "vscode-db2i.examples", + "title": "Examples", + "properties": { + "vscode-db2i.examples.customExampleDirectories": { + "type": "array", + "items": { + "type": "string", + "description": "The directory containing SQL example files." + }, + "markdownDescription": "Set of custom directories containing SQL example files to be shown in the `Examples` view. All SQL files in the specified directories and at most one subdirectory level deeper will be picked up.\n\nBy default, the folder name will be the category and the file name will be the name of the example. This can be customized by optionally including a comment at the top of the file with the tags `category` and `description`.", + "default": [] + } + } + }, { "id": "vscode-db2i.syntax", "title": "SQL Syntax Options", @@ -772,6 +787,12 @@ "category": "Db2 for i (Examples)", "icon": "$(clear-all)" }, + { + "command": "vscode-db2i.examples.reload", + "title": "Refresh Examples", + "category": "Db2 for i (Examples)", + "icon": "$(refresh)" + }, { "command": "vscode-db2i.notebook.fromSqlUri", "title": "Open as Notebook", @@ -999,13 +1020,13 @@ "when": "view == vscode-db2i.dove.node" }, { - "command": "vscode-db2i.queryHistory.clear", - "group": "navigation", + "command": "vscode-db2i.queryHistory.find", + "group": "navigation@0", "when": "view == queryHistory" }, { - "command": "vscode-db2i.queryHistory.find", - "group": "navigation", + "command": "vscode-db2i.queryHistory.clear", + "group": "navigation@1", "when": "view == queryHistory" }, { @@ -1070,12 +1091,17 @@ }, { "command": "vscode-db2i.examples.setFilter", - "group": "navigation", + "group": "navigation@0", "when": "view == exampleBrowser" }, { "command": "vscode-db2i.examples.clearFilter", - "group": "navigation", + "group": "navigation@1", + "when": "view == exampleBrowser" + }, + { + "command": "vscode-db2i.examples.reload", + "group": "navigation@2", "when": "view == exampleBrowser" } ], diff --git a/src/views/examples/contributes.json b/src/views/examples/contributes.json index af915ae4..4fdbd8e3 100644 --- a/src/views/examples/contributes.json +++ b/src/views/examples/contributes.json @@ -1,42 +1,70 @@ -{ - "contributes": { - "views": { - "db2-explorer": [ - { - "id": "exampleBrowser", - "name": "Examples", - "visibility": "collapsed", - "when": "code-for-ibmi:connected == true" - } - ] - }, - "commands": [ - { - "command": "vscode-db2i.examples.setFilter", - "title": "Set filter", - "category": "Db2 for i (Examples)", - "icon": "$(filter)" - }, - { - "command": "vscode-db2i.examples.clearFilter", - "title": "Clear filter", - "category": "Db2 for i (Examples)", - "icon": "$(clear-all)" - } - ], - "menus": { - "view/title": [ - { - "command": "vscode-db2i.examples.setFilter", - "group": "navigation", - "when": "view == exampleBrowser" - }, - { - "command": "vscode-db2i.examples.clearFilter", - "group": "navigation", - "when": "view == exampleBrowser" - } - ] - } - } +{ + "contributes": { + "views": { + "db2-explorer": [ + { + "id": "exampleBrowser", + "name": "Examples", + "visibility": "collapsed", + "when": "code-for-ibmi:connected == true" + } + ] + }, + "commands": [ + { + "command": "vscode-db2i.examples.setFilter", + "title": "Set filter", + "category": "Db2 for i (Examples)", + "icon": "$(filter)" + }, + { + "command": "vscode-db2i.examples.clearFilter", + "title": "Clear filter", + "category": "Db2 for i (Examples)", + "icon": "$(clear-all)" + }, + { + "command": "vscode-db2i.examples.reload", + "title": "Refresh Examples", + "category": "Db2 for i (Examples)", + "icon": "$(refresh)" + } + ], + "menus": { + "view/title": [ + { + "command": "vscode-db2i.examples.setFilter", + "group": "navigation@0", + "when": "view == exampleBrowser" + }, + { + "command": "vscode-db2i.examples.clearFilter", + "group": "navigation@1", + "when": "view == exampleBrowser" + }, + { + "command": "vscode-db2i.examples.reload", + "group": "navigation@2", + "when": "view == exampleBrowser" + } + ] + }, + "configuration": [ + { + "id": "vscode-db2i.examples", + "title": "Examples", + "properties": { + "vscode-db2i.examples.customExampleDirectories": { + "type": "array", + "items": { + "type": "string", + "description": "The directory containing SQL example files." + }, + "markdownDescription": "Set of custom directories containing SQL example files to be shown in the `Examples` view. All SQL files in the specified directories and at most one subdirectory level deeper will be picked up.\n\nBy default, the folder name will be the category and the file name will be the name of the example. This can be customized by optionally including a comment at the top of the file with the tags `category` and `description`.", + "default": [] + } + } + } + ] + } } \ No newline at end of file From 2c84a3c08d43d6a029effb8dffc86c6c364007e4 Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Tue, 26 Aug 2025 20:50:50 -0400 Subject: [PATCH 02/11] Initial work to load custom examples Signed-off-by: Sanjula Ganepola --- src/views/examples/exampleBrowser.ts | 278 ++++++++++++++------------- src/views/examples/index.ts | 94 +++++++++ 2 files changed, 234 insertions(+), 138 deletions(-) diff --git a/src/views/examples/exampleBrowser.ts b/src/views/examples/exampleBrowser.ts index 3f6211e1..946e8333 100644 --- a/src/views/examples/exampleBrowser.ts +++ b/src/views/examples/exampleBrowser.ts @@ -1,139 +1,141 @@ -import { Event, EventEmitter, ExtensionContext, MarkdownString, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, commands, window, workspace } from "vscode"; -import { Examples, SQLExample, ServiceInfoLabel } from "."; -import { getServiceInfo } from "../../database/serviceInfo"; -import { notebookFromStatements } from "../../notebooks/logic/openAsNotebook"; -import { osDetail } from "../../config"; - -export const openExampleCommand = `vscode-db2i.examples.open`; - -export class ExampleBrowser implements TreeDataProvider { - private _onDidChangeTreeData: EventEmitter = new EventEmitter(); - readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; - - private currentFilter: string | undefined; - - constructor(context: ExtensionContext) { - context.subscriptions.push( - commands.registerCommand(openExampleCommand, (example: SQLExample) => { - if (example) { - if (example.isNotebook) { - notebookFromStatements(example.content); - } else { - workspace.openTextDocument({ - content: example.content.join(`\n`), - language: `sql` - }).then(doc => { - window.showTextDocument(doc); - }); - } - } - }), - - commands.registerCommand(`vscode-db2i.examples.setFilter`, async () => { - this.currentFilter = await window.showInputBox({ - title: `Example Filter`, - prompt: `Enter filter criteria`, - value: this.currentFilter, - }); - - this.refresh(); - }), - - commands.registerCommand(`vscode-db2i.examples.clearFilter`, async () => { - this.currentFilter = undefined; - this.refresh(); - }), - - commands.registerCommand("vscode-db2i.examples.reload", () => { - delete Examples[ServiceInfoLabel]; - this.refresh(); - }) - ); - } - - refresh() { - this._onDidChangeTreeData.fire(); - } - - getTreeItem(element: any): TreeItem | Thenable { - return element; - } - - async getChildren(element?: ExampleGroupItem): Promise { - if (element) { - return element.getChildren(); - } - else { - // Unlike the bulk of the examples which are defined in views/examples/index.ts, the services examples are retrieved dynamically - if (!Examples[ServiceInfoLabel]) { - Examples[ServiceInfoLabel] = await getServiceInfo(); - } - - if (this.currentFilter) { - // If there is a filter, then show all examples that include this criteria - const upperFilter = this.currentFilter.toUpperCase(); - return Object.values(Examples) - .flatMap(examples => examples.filter(exampleWorksForOnOS)) - .filter(example => example.name.toUpperCase().includes(upperFilter) || example.content.some(line => line.toUpperCase().includes(upperFilter))) - .sort(sort) - .map(example => new SQLExampleItem(example)); - } - else { - return Object.entries(Examples) - .sort(([name1], [name2]) => sort(name1, name2)) - .map(([name, examples]) => new ExampleGroupItem(name, examples)); - } - } - } -} - -class ExampleGroupItem extends TreeItem { - constructor(name: string, private group: SQLExample[]) { - super(name, TreeItemCollapsibleState.Collapsed); - this.iconPath = ThemeIcon.Folder; - } - - getChildren(): SQLExampleItem[] { - return this.group - .filter(example => exampleWorksForOnOS(example)) - .sort(sort) - .map(example => new SQLExampleItem(example)); - } -} - -class SQLExampleItem extends TreeItem { - constructor(example: SQLExample) { - super(example.name, TreeItemCollapsibleState.None); - this.iconPath = ThemeIcon.File; - this.resourceUri = Uri.parse('_.sql'); - this.tooltip = new MarkdownString(['```sql', example.content.join(`\n`), '```'].join(`\n`)); - - this.command = { - command: openExampleCommand, - title: `Open example`, - arguments: [example] - }; - } -} - -function exampleWorksForOnOS(example: SQLExample): boolean { - - if (osDetail) { - const myOsVersion = osDetail.getVersion(); - - // If this example has specific system requirements defined - if (example.requirements && - example.requirements[myOsVersion] && - osDetail.getDb2Level() < example.requirements[myOsVersion]) { - return false; - } - } - - return true; -} - -function sort(string1: string | SQLExample, string2: string | SQLExample) { - string1 = typeof string1 === "string" ? string1 : string1.name; - string2 = typeof string2 === "string" ? string2 : string2.name; - return string1.localeCompare(string2); +import { Event, EventEmitter, ExtensionContext, MarkdownString, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, commands, window, workspace } from "vscode"; +import { Examples, SQLExample, ServiceInfoLabel, getMergedExamples } from "."; +import { notebookFromStatements } from "../../notebooks/logic/openAsNotebook"; +import { osDetail } from "../../config"; + +export const openExampleCommand = `vscode-db2i.examples.open`; + +export class ExampleBrowser implements TreeDataProvider { + private _onDidChangeTreeData: EventEmitter = new EventEmitter(); + readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; + + private currentFilter: string | undefined; + + constructor(context: ExtensionContext) { + context.subscriptions.push( + commands.registerCommand(openExampleCommand, (example: SQLExample) => { + if (example) { + if (example.isNotebook) { + notebookFromStatements(example.content); + } else { + workspace.openTextDocument({ + content: example.content.join(`\n`), + language: `sql` + }).then(doc => { + window.showTextDocument(doc); + }); + } + } + }), + + commands.registerCommand(`vscode-db2i.examples.setFilter`, async () => { + this.currentFilter = await window.showInputBox({ + title: `Example Filter`, + prompt: `Enter filter criteria`, + value: this.currentFilter, + }); + + this.refresh(); + }), + + commands.registerCommand(`vscode-db2i.examples.clearFilter`, async () => { + this.currentFilter = undefined; + this.refresh(); + }), + + commands.registerCommand("vscode-db2i.examples.reload", () => { + delete Examples[ServiceInfoLabel]; + this.refresh(); + }), + + workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('vscode-db2i.examples.customExampleDirectories')) { + this.refresh(); + } + }), + ); + } + + refresh() { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: any): TreeItem | Thenable { + return element; + } + + async getChildren(element?: ExampleGroupItem): Promise { + if (element) { + return element.getChildren(); + } + else { + const mergedExamples = await getMergedExamples(); + + if (this.currentFilter) { + // If there is a filter, then show all examples that include this criteria + const upperFilter = this.currentFilter.toUpperCase(); + return Object.values(mergedExamples) + .flatMap(examples => examples.filter(exampleWorksForOnOS)) + .filter(example => example.name.toUpperCase().includes(upperFilter) || example.content.some(line => line.toUpperCase().includes(upperFilter))) + .sort(sort) + .map(example => new SQLExampleItem(example)); + } + else { + return Object.entries(mergedExamples) + .sort(([name1], [name2]) => sort(name1, name2)) + .map(([name, examples]) => new ExampleGroupItem(name, examples)); + } + } + } +} + +class ExampleGroupItem extends TreeItem { + constructor(name: string, private group: SQLExample[]) { + super(name, TreeItemCollapsibleState.Collapsed); + this.iconPath = ThemeIcon.Folder; + } + + getChildren(): SQLExampleItem[] { + return this.group + .filter(example => exampleWorksForOnOS(example)) + .sort(sort) + .map(example => new SQLExampleItem(example)); + } +} + +class SQLExampleItem extends TreeItem { + constructor(example: SQLExample) { + super(example.name, TreeItemCollapsibleState.None); + this.iconPath = ThemeIcon.File; + this.resourceUri = Uri.parse('_.sql'); + this.tooltip = new MarkdownString(['```sql', example.content.join(`\n`), '```'].join(`\n`)); + + this.command = { + command: openExampleCommand, + title: `Open example`, + arguments: [example] + }; + } +} + +function exampleWorksForOnOS(example: SQLExample): boolean { + + if (osDetail) { + const myOsVersion = osDetail.getVersion(); + + // If this example has specific system requirements defined + if (example.requirements && + example.requirements[myOsVersion] && + osDetail.getDb2Level() < example.requirements[myOsVersion]) { + return false; + } + } + + return true; +} + +function sort(string1: string | SQLExample, string2: string | SQLExample) { + string1 = typeof string1 === "string" ? string1 : string1.name; + string2 = typeof string2 === "string" ? string2 : string2.name; + return string1.localeCompare(string2); } \ No newline at end of file diff --git a/src/views/examples/index.ts b/src/views/examples/index.ts index 997e61a7..ab2ee2dd 100644 --- a/src/views/examples/index.ts +++ b/src/views/examples/index.ts @@ -1,3 +1,8 @@ +import { EndOfLine, FileType, TextDocument, Uri, workspace } from "vscode"; +import Configuration from "../../configuration"; +import * as path from "path"; +import { getServiceInfo } from "../../database/serviceInfo"; + export interface SQLExamplesList { [group: string]: SQLExample[] } @@ -5978,4 +5983,93 @@ export const Examples: SQLExamplesList = { ] } ] +} + +export async function getMergedExamples(): Promise { + const defaultExamples = await getDefaultExamples(); + const customExamples = await getCustomExamples(); + const mergedExamples: SQLExamplesList = {}; + + for (const [group, examples] of Object.entries(defaultExamples)) { + mergedExamples[group] = [...examples]; + } + + for (const [group, examples] of Object.entries(customExamples)) { + if (mergedExamples[group]) { + mergedExamples[group] = [...mergedExamples[group], ...examples]; + } else { + mergedExamples[group] = [...examples]; + } + } + + return mergedExamples; +} + +async function getDefaultExamples(): Promise { + // Unlike the bulk of the examples which are defined in views/examples/index.ts, the services examples are retrieved dynamically + if (!Examples[ServiceInfoLabel]) { + Examples[ServiceInfoLabel] = await getServiceInfo(); + } + + return Examples; +} + +export async function getCustomExamples(): Promise { + // Get custom example directories from VS Code settings + const customDirectoryPaths = Configuration.get(`examples.customExampleDirectories`) || []; + if (customDirectoryPaths.length === 0) { + return {}; + } + + // Get all SQL files from the specified directories (include 1 level of subdirectories) + const sqlTextDocuments: TextDocument[] = []; + for (const dirPath of customDirectoryPaths) { + const dirUri = Uri.file(dirPath); + const dirTextDocuments = await getSqlTextDocumentsFromDirectory(dirUri); + sqlTextDocuments.push(...dirTextDocuments); + } + + // Organize the SQL files into groups based on their parent directory names + const examplesList: SQLExamplesList = {}; + for (const textDocument of sqlTextDocuments) { + const parsedPath = path.parse(textDocument.uri.path); + const group = path.basename(parsedPath.dir); + const name = parsedPath.name; + + if (!examplesList[group]) { + examplesList[group] = []; + } + + examplesList[group].push({ + name: name, + content: textDocument.getText().split(textDocument.eol === EndOfLine.LF ? '\n' : '\r\n') + }); + } + + return examplesList; +} + +async function getSqlTextDocumentsFromDirectory(directory: Uri, depth = 1): Promise { + const sqlTextDocuments: TextDocument[] = []; + + try { + const contents = await workspace.fs.readDirectory(directory); + for (const [name, type] of contents) { + const uri = Uri.joinPath(directory, name); + + if (type === FileType.File) { + const textDocument = await workspace.openTextDocument(uri); + if (textDocument.languageId === 'sql') { + sqlTextDocuments.push(textDocument); + } + } else if (type === FileType.Directory && depth > 0) { + const subContents = await getSqlTextDocumentsFromDirectory(uri, depth - 1); + sqlTextDocuments.push(...subContents); + } + } + } catch { + // Ignore errors + } + + return sqlTextDocuments; } \ No newline at end of file From f697ee23ac381944bbc5c6811e9701b6be290b62 Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Tue, 26 Aug 2025 20:51:18 -0400 Subject: [PATCH 03/11] Re-order statement history actions Signed-off-by: Sanjula Ganepola --- src/views/queryHistoryView/contributes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/queryHistoryView/contributes.json b/src/views/queryHistoryView/contributes.json index 1a37c109..781d9c72 100644 --- a/src/views/queryHistoryView/contributes.json +++ b/src/views/queryHistoryView/contributes.json @@ -59,13 +59,13 @@ ], "view/title": [ { - "command": "vscode-db2i.queryHistory.clear", - "group": "navigation", + "command": "vscode-db2i.queryHistory.find", + "group": "navigation@0", "when": "view == queryHistory" }, { - "command": "vscode-db2i.queryHistory.find", - "group": "navigation", + "command": "vscode-db2i.queryHistory.clear", + "group": "navigation@1", "when": "view == queryHistory" } ], From c3eb49f956b85814c76521a507fbc736436a8e41 Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Tue, 26 Aug 2025 21:05:21 -0400 Subject: [PATCH 04/11] Add support for comment overrides Signed-off-by: Sanjula Ganepola --- package.json | 2 +- src/views/examples/contributes.json | 2 +- src/views/examples/index.ts | 21 ++++++++++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b7cfdf59..a70171d0 100644 --- a/package.json +++ b/package.json @@ -265,7 +265,7 @@ "type": "string", "description": "The directory containing SQL example files." }, - "markdownDescription": "Set of custom directories containing SQL example files to be shown in the `Examples` view. All SQL files in the specified directories and at most one subdirectory level deeper will be picked up.\n\nBy default, the folder name will be the category and the file name will be the name of the example. This can be customized by optionally including a comment at the top of the file with the tags `category` and `description`.", + "markdownDescription": "Set of custom directories containing SQL example files to be shown in the `Examples` view. All SQL files in the specified directories and at most one subdirectory level deeper will be picked up.\n\nBy default, the folder name will be the category and the file name will be the name of the example. This can be customized by optionally including a comment in the file with the tags `category` and `description`.", "default": [] } } diff --git a/src/views/examples/contributes.json b/src/views/examples/contributes.json index 4fdbd8e3..aeef097d 100644 --- a/src/views/examples/contributes.json +++ b/src/views/examples/contributes.json @@ -60,7 +60,7 @@ "type": "string", "description": "The directory containing SQL example files." }, - "markdownDescription": "Set of custom directories containing SQL example files to be shown in the `Examples` view. All SQL files in the specified directories and at most one subdirectory level deeper will be picked up.\n\nBy default, the folder name will be the category and the file name will be the name of the example. This can be customized by optionally including a comment at the top of the file with the tags `category` and `description`.", + "markdownDescription": "Set of custom directories containing SQL example files to be shown in the `Examples` view. All SQL files in the specified directories and at most one subdirectory level deeper will be picked up.\n\nBy default, the folder name will be the category and the file name will be the name of the example. This can be customized by optionally including a comment in the file with the tags `category` and `description`.", "default": [] } } diff --git a/src/views/examples/index.ts b/src/views/examples/index.ts index ab2ee2dd..4076181d 100644 --- a/src/views/examples/index.ts +++ b/src/views/examples/index.ts @@ -2,6 +2,7 @@ import { EndOfLine, FileType, TextDocument, Uri, workspace } from "vscode"; import Configuration from "../../configuration"; import * as path from "path"; import { getServiceInfo } from "../../database/serviceInfo"; +import { getStatementDetail } from "../../notebooks/logic/statement"; export interface SQLExamplesList { [group: string]: SQLExample[] @@ -6032,9 +6033,23 @@ export async function getCustomExamples(): Promise { // Organize the SQL files into groups based on their parent directory names const examplesList: SQLExamplesList = {}; for (const textDocument of sqlTextDocuments) { + // Set group and name based on file path by default const parsedPath = path.parse(textDocument.uri.path); - const group = path.basename(parsedPath.dir); - const name = parsedPath.name; + let group = path.basename(parsedPath.dir); + let name = parsedPath.name; + + // Override group and name if category and description is specified in the SQL file as comments + const content = textDocument.getText(); + const eol = textDocument.eol === EndOfLine.LF ? '\n' : '\r\n'; + const detail = getStatementDetail(content, eol); + const category = detail?.settings.category; + if (category) { + group = category; + } + const description = detail?.settings.description; + if (description) { + name = description; + } if (!examplesList[group]) { examplesList[group] = []; @@ -6042,7 +6057,7 @@ export async function getCustomExamples(): Promise { examplesList[group].push({ name: name, - content: textDocument.getText().split(textDocument.eol === EndOfLine.LF ? '\n' : '\r\n') + content: textDocument.getText().split(eol) }); } From 09dba7905c512e1641f2a3519bb3ac325371f2a5 Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Wed, 27 Aug 2025 00:35:07 -0400 Subject: [PATCH 05/11] New actions to add, remove, edit Signed-off-by: Sanjula Ganepola --- package.json | 44 ++++++++++++++++++++++++- src/views/examples/contributes.json | 48 ++++++++++++++++++++++++++- src/views/examples/exampleBrowser.ts | 49 ++++++++++++++++++++++++++-- src/views/examples/index.ts | 4 ++- 4 files changed, 140 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a70171d0..575b11d0 100644 --- a/package.json +++ b/package.json @@ -793,6 +793,21 @@ "category": "Db2 for i (Examples)", "icon": "$(refresh)" }, + { + "command": "vscode-db2i.examples.add", + "title": "Add...", + "category": "Db2 for i (Examples)" + }, + { + "command": "vscode-db2i.examples.remove", + "title": "Remove...", + "category": "Db2 for i (Examples)" + }, + { + "command": "vscode-db2i.examples.edit", + "title": "Edit Example", + "category": "Db2 for i (Examples)" + }, { "command": "vscode-db2i.notebook.fromSqlUri", "title": "Open as Notebook", @@ -1100,9 +1115,14 @@ "when": "view == exampleBrowser" }, { - "command": "vscode-db2i.examples.reload", + "submenu": "vscode-db2i.customExampleDirectories", "group": "navigation@2", "when": "view == exampleBrowser" + }, + { + "command": "vscode-db2i.examples.reload", + "group": "navigation@3", + "when": "view == exampleBrowser" } ], "view/item/context": [ @@ -1274,6 +1294,11 @@ "command": "vscode-db2i.self.explainSelf", "when": "view == vscode-db2i.self.nodes && viewItem == selfCodeNode && vscode-db2i:continueExtensionActive", "group": "navigation" + }, + { + "command": "vscode-db2i.examples.edit", + "when": "view == exampleBrowser && viewItem == example.custom", + "group": "0_open" } ], "editor/title": [ @@ -1348,6 +1373,11 @@ "icon": "$(notebook-execute)", "id": "sql/editor/context", "label": "Run SQL statement" + }, + { + "id": "vscode-db2i.customExampleDirectories", + "label": "Custom Example Directories", + "icon": "$(sparkle)" } ], "keybindings": [ @@ -1470,6 +1500,18 @@ "contents": "🛠️ SELF Codes will appear here. You can set the SELF log level on specific jobs, or you can set the default for new jobs in the User Settings.\n\n[Set Default for New Jobs](command:vscode-db2i.jobManager.defaultSettings)\n\n[Learn about SELF](command:vscode-db2i.self.help)" } ], + "vscode-db2i.customExampleDirectories": [ + { + "command": "vscode-db2i.examples.add", + "group": "navigation@0", + "when": "view == exampleBrowser" + }, + { + "command": "vscode-db2i.examples.remove", + "group": "navigation@1", + "when": "view == exampleBrowser" + } + ], "notebooks": [ { "id": "db2i-notebook", diff --git a/src/views/examples/contributes.json b/src/views/examples/contributes.json index aeef097d..7793bea5 100644 --- a/src/views/examples/contributes.json +++ b/src/views/examples/contributes.json @@ -28,6 +28,40 @@ "title": "Refresh Examples", "category": "Db2 for i (Examples)", "icon": "$(refresh)" + }, + { + "command": "vscode-db2i.examples.add", + "title": "Add...", + "category": "Db2 for i (Examples)" + }, + { + "command": "vscode-db2i.examples.remove", + "title": "Remove...", + "category": "Db2 for i (Examples)" + }, + { + "command": "vscode-db2i.examples.edit", + "title": "Edit Example", + "category": "Db2 for i (Examples)" + } + ], + "submenus": [ + { + "id": "vscode-db2i.customExampleDirectories", + "label": "Custom Example Directories", + "icon": "$(sparkle)" + } + ], + "vscode-db2i.customExampleDirectories": [ + { + "command": "vscode-db2i.examples.add", + "group": "navigation@0", + "when": "view == exampleBrowser" + }, + { + "command": "vscode-db2i.examples.remove", + "group": "navigation@1", + "when": "view == exampleBrowser" } ], "menus": { @@ -43,9 +77,21 @@ "when": "view == exampleBrowser" }, { - "command": "vscode-db2i.examples.reload", + "submenu": "vscode-db2i.customExampleDirectories", "group": "navigation@2", "when": "view == exampleBrowser" + }, + { + "command": "vscode-db2i.examples.reload", + "group": "navigation@3", + "when": "view == exampleBrowser" + } + ], + "view/item/context": [ + { + "command": "vscode-db2i.examples.edit", + "when": "view == exampleBrowser && viewItem == example.custom", + "group": "0_open" } ] }, diff --git a/src/views/examples/exampleBrowser.ts b/src/views/examples/exampleBrowser.ts index 946e8333..e1d95ab0 100644 --- a/src/views/examples/exampleBrowser.ts +++ b/src/views/examples/exampleBrowser.ts @@ -2,6 +2,7 @@ import { Event, EventEmitter, ExtensionContext, MarkdownString, ThemeIcon, TreeD import { Examples, SQLExample, ServiceInfoLabel, getMergedExamples } from "."; import { notebookFromStatements } from "../../notebooks/logic/openAsNotebook"; import { osDetail } from "../../config"; +import Configuration from "../../configuration"; export const openExampleCommand = `vscode-db2i.examples.open`; @@ -48,6 +49,49 @@ export class ExampleBrowser implements TreeDataProvider { this.refresh(); }), + commands.registerCommand("vscode-db2i.examples.add", async () => { + const dirsToAdd = await window.showOpenDialog({ + title: "Add Custom Example Directory", + canSelectFolders: true, + canSelectFiles: false, + canSelectMany: true + }); + + if (dirsToAdd && dirsToAdd.length > 0) { + const existingDirectories = Configuration.get(`examples.customExampleDirectories`) || []; + const newDirectoryPaths = dirsToAdd.map(dir => dir.fsPath); + const updatedDirectories = Array.from(new Set([...existingDirectories, ...newDirectoryPaths])); + await Configuration.set(`examples.customExampleDirectories`, updatedDirectories); + } + }), + + commands.registerCommand("vscode-db2i.examples.remove", async () => { + const existingDirectories = Configuration.get(`examples.customExampleDirectories`) || []; + if (existingDirectories.length === 0) { + window.showErrorMessage(`No custom example directories to remove.`); + return; + } + + const quickPickItems = existingDirectories.map(dir => ({ label: dir })); + const selectedDirectories = await window.showQuickPick(quickPickItems, { + title: `Select custom example directories to remove`, + canPickMany: true + }); + + if (selectedDirectories && selectedDirectories.length > 0) { + const dirsToRemove = selectedDirectories.map(item => item.label); + const updatedDirectories = existingDirectories.filter(dir => !dirsToRemove.includes(dir)); + await Configuration.set(`examples.customExampleDirectories`, updatedDirectories); + } + }), + + commands.registerCommand("vscode-db2i.examples.edit", async (item: SQLExampleItem) => { + if (item.example.customFileUri) { + const document = await workspace.openTextDocument(item.example.customFileUri); + await window.showTextDocument(document); + } + }), + workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('vscode-db2i.examples.customExampleDirectories')) { this.refresh(); @@ -64,7 +108,7 @@ export class ExampleBrowser implements TreeDataProvider { return element; } - async getChildren(element?: ExampleGroupItem): Promise { + async getChildren(element?: ExampleGroupItem): Promise { if (element) { return element.getChildren(); } @@ -104,11 +148,12 @@ class ExampleGroupItem extends TreeItem { } class SQLExampleItem extends TreeItem { - constructor(example: SQLExample) { + constructor(public example: SQLExample) { super(example.name, TreeItemCollapsibleState.None); this.iconPath = ThemeIcon.File; this.resourceUri = Uri.parse('_.sql'); this.tooltip = new MarkdownString(['```sql', example.content.join(`\n`), '```'].join(`\n`)); + this.contextValue = example.customFileUri ? `example.custom` : `example`; this.command = { command: openExampleCommand, diff --git a/src/views/examples/index.ts b/src/views/examples/index.ts index 4076181d..622d312f 100644 --- a/src/views/examples/index.ts +++ b/src/views/examples/index.ts @@ -18,6 +18,7 @@ export interface SQLExample { content: string[]; requirements?: ExampleSystemRequirements; isNotebook?: boolean; + customFileUri?: Uri; }; // Unlike the bulk of the examples defined below, the services examples are retrieved dynamically @@ -6057,7 +6058,8 @@ export async function getCustomExamples(): Promise { examplesList[group].push({ name: name, - content: textDocument.getText().split(eol) + content: textDocument.getText().split(eol), + customFileUri: textDocument.uri }); } From 06c786820fc6838d345f9f05f9e1e3f6deb4400d Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Thu, 28 Aug 2025 12:52:13 -0400 Subject: [PATCH 06/11] Add support for saving new examples Signed-off-by: Sanjula Ganepola --- package.json | 15 ++++++- src/views/examples/contributes.json | 17 ++++++-- src/views/examples/exampleBrowser.ts | 65 ++++++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 575b11d0..097c7b27 100644 --- a/package.json +++ b/package.json @@ -793,6 +793,12 @@ "category": "Db2 for i (Examples)", "icon": "$(refresh)" }, + { + "command": "vscode-db2i.examples.save", + "title": "Save As New Example", + "category": "Db2 for i (Examples)", + "icon": "$(save)" + }, { "command": "vscode-db2i.examples.add", "title": "Add...", @@ -1115,14 +1121,19 @@ "when": "view == exampleBrowser" }, { - "submenu": "vscode-db2i.customExampleDirectories", + "command": "vscode-db2i.examples.save", "group": "navigation@2", "when": "view == exampleBrowser" }, { - "command": "vscode-db2i.examples.reload", + "submenu": "vscode-db2i.customExampleDirectories", "group": "navigation@3", "when": "view == exampleBrowser" + }, + { + "command": "vscode-db2i.examples.reload", + "group": "navigation@4", + "when": "view == exampleBrowser" } ], "view/item/context": [ diff --git a/src/views/examples/contributes.json b/src/views/examples/contributes.json index 7793bea5..0e5ef447 100644 --- a/src/views/examples/contributes.json +++ b/src/views/examples/contributes.json @@ -29,6 +29,12 @@ "category": "Db2 for i (Examples)", "icon": "$(refresh)" }, + { + "command": "vscode-db2i.examples.save", + "title": "Save As New Example", + "category": "Db2 for i (Examples)", + "icon": "$(save)" + }, { "command": "vscode-db2i.examples.add", "title": "Add...", @@ -77,19 +83,24 @@ "when": "view == exampleBrowser" }, { - "submenu": "vscode-db2i.customExampleDirectories", + "command": "vscode-db2i.examples.save", "group": "navigation@2", "when": "view == exampleBrowser" }, { - "command": "vscode-db2i.examples.reload", + "submenu": "vscode-db2i.customExampleDirectories", "group": "navigation@3", "when": "view == exampleBrowser" + }, + { + "command": "vscode-db2i.examples.reload", + "group": "navigation@4", + "when": "view == exampleBrowser" } ], "view/item/context": [ { - "command": "vscode-db2i.examples.edit", + "command": "vscode-db2i.examples.edit", "when": "view == exampleBrowser && viewItem == example.custom", "group": "0_open" } diff --git a/src/views/examples/exampleBrowser.ts b/src/views/examples/exampleBrowser.ts index e1d95ab0..2a658500 100644 --- a/src/views/examples/exampleBrowser.ts +++ b/src/views/examples/exampleBrowser.ts @@ -1,8 +1,9 @@ -import { Event, EventEmitter, ExtensionContext, MarkdownString, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, commands, window, workspace } from "vscode"; +import { Event, EventEmitter, ExtensionContext, MarkdownString, TextDocument, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, commands, window, workspace } from "vscode"; import { Examples, SQLExample, ServiceInfoLabel, getMergedExamples } from "."; import { notebookFromStatements } from "../../notebooks/logic/openAsNotebook"; import { osDetail } from "../../config"; import Configuration from "../../configuration"; +import * as path from 'path'; export const openExampleCommand = `vscode-db2i.examples.open`; @@ -49,6 +50,61 @@ export class ExampleBrowser implements TreeDataProvider { this.refresh(); }), + commands.registerCommand("vscode-db2i.examples.save", async () => { + const editor = window.activeTextEditor; + if (editor) { + const document = editor.document; + if (document.languageId === `sql`) { + const existingDirectories = Configuration.get(`examples.customExampleDirectories`) || []; + if (existingDirectories.length === 0) { + window.showErrorMessage(`You must first add a custom examples directory before saving new examples.`, { modal: true }, `Add Custom Examples Directory`).then(async selection => { + if (selection === `Add Custom Examples Directory`) { + const isAdded = await commands.executeCommand(`vscode-db2i.examples.add`); + if (isAdded) { + await commands.executeCommand(`vscode-db2i.examples.save`); + } + + return; + }; + }); + } else { + const quickPickItems = existingDirectories.map(dir => ({ label: dir })); + const selectedDirectory = await window.showQuickPick(quickPickItems, { + title: `Select a custom example directories to save this example to` + }); + if (selectedDirectory) { + const suggestedFileName = path.basename(document.fileName); + let exampleFileName = await window.showInputBox({ + title: `Example file name`, + prompt: `Enter example file name`, + value: suggestedFileName, + }); + + if (exampleFileName) { + if (!exampleFileName.includes(`.`)) { + exampleFileName = `${exampleFileName}.sql`; + } + + try { + const filePath = Uri.joinPath(Uri.file(selectedDirectory.label), exampleFileName); + const fileContent = Buffer.from(document.getText(), 'utf8') + await workspace.fs.writeFile(filePath, fileContent); + window.showInformationMessage(`Example saved to ${filePath.fsPath}`); + this.refresh(); + } catch (error) { + window.showErrorMessage(`Failed to save example: ${error}`); + } + } + } + } + } else { + window.showErrorMessage(`The active document is not a SQL file.`); + } + } else { + window.showErrorMessage(`No SQL file open.`); + } + }), + commands.registerCommand("vscode-db2i.examples.add", async () => { const dirsToAdd = await window.showOpenDialog({ title: "Add Custom Example Directory", @@ -62,6 +118,9 @@ export class ExampleBrowser implements TreeDataProvider { const newDirectoryPaths = dirsToAdd.map(dir => dir.fsPath); const updatedDirectories = Array.from(new Set([...existingDirectories, ...newDirectoryPaths])); await Configuration.set(`examples.customExampleDirectories`, updatedDirectories); + return true; + } else { + return false; } }), @@ -74,7 +133,7 @@ export class ExampleBrowser implements TreeDataProvider { const quickPickItems = existingDirectories.map(dir => ({ label: dir })); const selectedDirectories = await window.showQuickPick(quickPickItems, { - title: `Select custom example directories to remove`, + title: `Select a custom example directories to remove`, canPickMany: true }); @@ -96,7 +155,7 @@ export class ExampleBrowser implements TreeDataProvider { if (e.affectsConfiguration('vscode-db2i.examples.customExampleDirectories')) { this.refresh(); } - }), + }) ); } From 429f93f0ed42bfd8a8045600657c8efafdd17eaa Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Thu, 28 Aug 2025 13:08:12 -0400 Subject: [PATCH 07/11] Move submenu contribution Signed-off-by: Sanjula Ganepola --- package.json | 24 +++++++++++------------- src/views/examples/contributes.json | 24 +++++++++++------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 097c7b27..5454a167 100644 --- a/package.json +++ b/package.json @@ -1371,6 +1371,16 @@ "group": "navigation_notebook@1" } ], + "vscode-db2i.customExampleDirectories": [ + { + "command": "vscode-db2i.examples.add", + "group": "navigation@0" + }, + { + "command": "vscode-db2i.examples.remove", + "group": "navigation@1" + } + ], "notebook/toolbar": [ { "command": "vscode-db2i.notebook.exportAsHtml", @@ -1388,7 +1398,7 @@ { "id": "vscode-db2i.customExampleDirectories", "label": "Custom Example Directories", - "icon": "$(sparkle)" + "icon": "$(folder-library)" } ], "keybindings": [ @@ -1511,18 +1521,6 @@ "contents": "🛠️ SELF Codes will appear here. You can set the SELF log level on specific jobs, or you can set the default for new jobs in the User Settings.\n\n[Set Default for New Jobs](command:vscode-db2i.jobManager.defaultSettings)\n\n[Learn about SELF](command:vscode-db2i.self.help)" } ], - "vscode-db2i.customExampleDirectories": [ - { - "command": "vscode-db2i.examples.add", - "group": "navigation@0", - "when": "view == exampleBrowser" - }, - { - "command": "vscode-db2i.examples.remove", - "group": "navigation@1", - "when": "view == exampleBrowser" - } - ], "notebooks": [ { "id": "db2i-notebook", diff --git a/src/views/examples/contributes.json b/src/views/examples/contributes.json index 0e5ef447..5e2438b2 100644 --- a/src/views/examples/contributes.json +++ b/src/views/examples/contributes.json @@ -55,19 +55,7 @@ { "id": "vscode-db2i.customExampleDirectories", "label": "Custom Example Directories", - "icon": "$(sparkle)" - } - ], - "vscode-db2i.customExampleDirectories": [ - { - "command": "vscode-db2i.examples.add", - "group": "navigation@0", - "when": "view == exampleBrowser" - }, - { - "command": "vscode-db2i.examples.remove", - "group": "navigation@1", - "when": "view == exampleBrowser" + "icon": "$(folder-library)" } ], "menus": { @@ -104,6 +92,16 @@ "when": "view == exampleBrowser && viewItem == example.custom", "group": "0_open" } + ], + "vscode-db2i.customExampleDirectories": [ + { + "command": "vscode-db2i.examples.add", + "group": "navigation@0" + }, + { + "command": "vscode-db2i.examples.remove", + "group": "navigation@1" + } ] }, "configuration": [ From a1b67e156b418a9563adbbc9bfd41cc4e8f072af Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Thu, 28 Aug 2025 14:05:50 -0400 Subject: [PATCH 08/11] Add watchers for examples directory Signed-off-by: Sanjula Ganepola --- src/views/examples/exampleBrowser.ts | 32 +++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/views/examples/exampleBrowser.ts b/src/views/examples/exampleBrowser.ts index 2a658500..aed538a6 100644 --- a/src/views/examples/exampleBrowser.ts +++ b/src/views/examples/exampleBrowser.ts @@ -1,4 +1,4 @@ -import { Event, EventEmitter, ExtensionContext, MarkdownString, TextDocument, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, commands, window, workspace } from "vscode"; +import { Event, EventEmitter, ExtensionContext, FileSystemWatcher, MarkdownString, RelativePattern, TextDocument, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, commands, window, workspace } from "vscode"; import { Examples, SQLExample, ServiceInfoLabel, getMergedExamples } from "."; import { notebookFromStatements } from "../../notebooks/logic/openAsNotebook"; import { osDetail } from "../../config"; @@ -11,9 +11,12 @@ export class ExampleBrowser implements TreeDataProvider { private _onDidChangeTreeData: EventEmitter = new EventEmitter(); readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; + private watchers: FileSystemWatcher[] = []; private currentFilter: string | undefined; constructor(context: ExtensionContext) { + this.setupWatchers(); + context.subscriptions.push( commands.registerCommand(openExampleCommand, (example: SQLExample) => { if (example) { @@ -153,6 +156,7 @@ export class ExampleBrowser implements TreeDataProvider { workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('vscode-db2i.examples.customExampleDirectories')) { + this.setupWatchers(); this.refresh(); } }) @@ -190,6 +194,32 @@ export class ExampleBrowser implements TreeDataProvider { } } } + + setupWatchers() { + if (this.watchers) { + for (const watcher of this.watchers) { + watcher.dispose(); + } + this.watchers = []; + } + + const existingDirectories = Configuration.get(`examples.customExampleDirectories`) || []; + for (const directory of existingDirectories) { + const directoryUri = Uri.file(directory); + const relativePattern = new RelativePattern(directoryUri, '**/*'); + const watcher = workspace.createFileSystemWatcher(relativePattern); + watcher.onDidCreate(async (uri) => { + this.refresh(); + }); + watcher.onDidChange(async (uri) => { + this.refresh(); + }); + watcher.onDidDelete(async (uri) => { + this.refresh(); + }); + this.watchers.push(watcher); + } + } } class ExampleGroupItem extends TreeItem { From 90a4e126d8911344c7080cdbdee229093405bf81 Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Thu, 28 Aug 2025 15:35:29 -0400 Subject: [PATCH 09/11] Remove edit action form command palette Signed-off-by: Sanjula Ganepola --- package.json | 4 ++++ src/views/examples/contributes.json | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/package.json b/package.json index 5454a167..118060a2 100644 --- a/package.json +++ b/package.json @@ -972,6 +972,10 @@ "command": "vscode-db2i.jobManager.deleteConfig", "when": "never" }, + { + "command": "vscode-db2i.examples.edit", + "when": "never" + }, { "command": "vscode-db2i.notebook.fromSqlUri", "when": "never" diff --git a/src/views/examples/contributes.json b/src/views/examples/contributes.json index 5e2438b2..cd95175e 100644 --- a/src/views/examples/contributes.json +++ b/src/views/examples/contributes.json @@ -102,6 +102,12 @@ "command": "vscode-db2i.examples.remove", "group": "navigation@1" } + ], + "commandPalette": [ + { + "command": "vscode-db2i.examples.edit", + "when": "never" + } ] }, "configuration": [ From a4421c26e267e92d1c6f1c4bef1417057d8d46dc Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Fri, 29 Aug 2025 12:16:43 -0400 Subject: [PATCH 10/11] Fix bug with reading binary file Signed-off-by: Sanjula Ganepola --- src/views/examples/index.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/views/examples/index.ts b/src/views/examples/index.ts index 622d312f..aa725cfb 100644 --- a/src/views/examples/index.ts +++ b/src/views/examples/index.ts @@ -6075,17 +6075,24 @@ async function getSqlTextDocumentsFromDirectory(directory: Uri, depth = 1): Prom const uri = Uri.joinPath(directory, name); if (type === FileType.File) { - const textDocument = await workspace.openTextDocument(uri); - if (textDocument.languageId === 'sql') { - sqlTextDocuments.push(textDocument); + try { + const textDocument = await workspace.openTextDocument(uri); + if (textDocument.languageId === 'sql') { + sqlTextDocuments.push(textDocument); + } + } catch (error) { + // Ignore error reading file + console.log(error); + continue; } } else if (type === FileType.Directory && depth > 0) { const subContents = await getSqlTextDocumentsFromDirectory(uri, depth - 1); sqlTextDocuments.push(...subContents); } } - } catch { - // Ignore errors + } catch (error) { + // Ignore error reading directory + console.log(error); } return sqlTextDocuments; From 970deb56d7d54041729287fd1358793fc760db09 Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Fri, 29 Aug 2025 12:59:25 -0400 Subject: [PATCH 11/11] Fix getStatementDetail when no code present Signed-off-by: Sanjula Ganepola --- src/notebooks/logic/statement.ts | 113 ++++++++++++++++--------------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/src/notebooks/logic/statement.ts b/src/notebooks/logic/statement.ts index 7a33d88f..aecae4db 100644 --- a/src/notebooks/logic/statement.ts +++ b/src/notebooks/logic/statement.ts @@ -1,57 +1,58 @@ -import { ChartDetail, chartTypes } from "./chart"; -import { ChartJsType, chartJsTypes } from "./chartJs"; - -export interface StatementSettings { - chart?: ChartJsType; - title?: string; - y?: string; - hideStatement?: string; - [key: string]: string -}; - -export function getStatementDetail(content: string, eol: string) { - let chartDetail: ChartDetail = {}; - let settings: StatementSettings = {}; - - // Strip out starting comments - if (content.startsWith(`--`)) { - const lines = content.split(eol); - const firstNonCommentLine = lines.findIndex(line => !line.startsWith(`--`)); - - const startingComments = lines.slice(0, firstNonCommentLine).map(line => line.substring(2).trim()); - content = lines.slice(firstNonCommentLine).join(eol); - - for (let comment of startingComments) { - const sep = comment.indexOf(`:`); - const key = comment.substring(0, sep).trim(); - const value = comment.substring(sep + 1).trim(); - settings[key] = value; - } - - // Chart settings defined by comments - if (settings[`chart`] && chartJsTypes.includes(settings[`chart`])) { - chartDetail.type = settings[`chart`]; - } - - if (settings[`title`]) { - chartDetail.title = settings[`title`]; - } - - if (settings[`y`]) { - chartDetail.y = settings[`y`]; - } - } - - // Remove trailing semicolon. The Service Component doesn't like it. - if (content.endsWith(`;`)) { - content = content.substring(0, content.length - 1); - } - - // Perhaps the chart type is defined by the statement prefix - const chartType: ChartJsType | undefined = chartTypes.find(type => content.startsWith(`${type}:`)); - if (chartType) { - chartDetail.type = chartType; - content = content.substring(chartType.length + 1); - } - return { chartDetail, content, settings }; +import { ChartDetail, chartTypes } from "./chart"; +import { ChartJsType, chartJsTypes } from "./chartJs"; + +export interface StatementSettings { + chart?: ChartJsType; + title?: string; + y?: string; + hideStatement?: string; + [key: string]: string +}; + +export function getStatementDetail(content: string, eol: string) { + let chartDetail: ChartDetail = {}; + let settings: StatementSettings = {}; + + // Strip out starting comments + if (content.startsWith(`--`)) { + const lines = content.split(eol); + const firstNonCommentLine = lines.findIndex(line => !line.startsWith(`--`)); + + const startingCommentLines = firstNonCommentLine === -1 ? lines : lines.slice(0, firstNonCommentLine); + const startingComments = startingCommentLines.map(line => line.substring(2).trim()); + content = lines.slice(firstNonCommentLine).join(eol); + + for (let comment of startingComments) { + const sep = comment.indexOf(`:`); + const key = comment.substring(0, sep).trim(); + const value = comment.substring(sep + 1).trim(); + settings[key] = value; + } + + // Chart settings defined by comments + if (settings[`chart`] && chartJsTypes.includes(settings[`chart`])) { + chartDetail.type = settings[`chart`]; + } + + if (settings[`title`]) { + chartDetail.title = settings[`title`]; + } + + if (settings[`y`]) { + chartDetail.y = settings[`y`]; + } + } + + // Remove trailing semicolon. The Service Component doesn't like it. + if (content.endsWith(`;`)) { + content = content.substring(0, content.length - 1); + } + + // Perhaps the chart type is defined by the statement prefix + const chartType: ChartJsType | undefined = chartTypes.find(type => content.startsWith(`${type}:`)); + if (chartType) { + chartDetail.type = chartType; + content = content.substring(chartType.length + 1); + } + return { chartDetail, content, settings }; } \ No newline at end of file