diff --git a/CHANGELOG.md b/CHANGELOG.md index 96aaddbc9..f55ed822b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Add the ability to sort columns in data viewer and view column properties ([#1622](https://github.com/sassoftware/vscode-sas-extension/pull/1622)) - Add code comment collapsing ([#1638](https://github.com/sassoftware/vscode-sas-extension/pull/1638)) - Add ability to view dataset properties ([#1631](https://github.com/sassoftware/vscode-sas-extension/pull/1631)) +- Add R language support for PROC RLANG (syntax highlighting, notebook cells, code formatting preservation) ### Fixed diff --git a/README.md b/README.md index 122544562..ba6de89fd 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The SAS extension includes many [features](https://sassoftware.github.io/vscode- - SAS syntax highlighting and help, code completion, and code snippets - Navigate SAS Content and libraries, including table viewer -- Create notebooks for SAS, SQL, Python and other languages +- Create notebooks for SAS, SQL, Python, R, and other languages diff --git a/client/package-lock.json b/client/package-lock.json index 8dc506818..60a83aeb9 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -47,6 +47,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -865,6 +866,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -874,6 +876,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, diff --git a/client/src/components/ContentNavigator/convert.ts b/client/src/components/ContentNavigator/convert.ts index 4a8783e2a..b35214b49 100644 --- a/client/src/components/ContentNavigator/convert.ts +++ b/client/src/components/ContentNavigator/convert.ts @@ -20,12 +20,16 @@ const stepRef: Record = { sas: "a7190700-f59c-4a94-afe2-214ce639fcde", sql: "a7190700-f59c-4a94-afe2-214ce639fcde", python: "ab59f8c4-af9a-4608-a5d5-a8365357bb99", + rlang: "ab59f8c4-af9a-4608-a5d5-a8365357bb99", + julia: "ab59f8c4-af9a-4608-a5d5-a8365357bb99", }; const stepTitle: Record = { sas: l10n.t("SAS Program"), sql: l10n.t("SQL Program"), python: l10n.t("Python Program"), + rlang: l10n.t("R Program"), + julia: l10n.t("Julia Program"), }; const NODE_SPACING = 150; @@ -305,7 +309,7 @@ function generateCodeListFromSASNotebook(content: string): Entry[] { let code = cell.value; if (code !== "") { const language = cell.language; - if (["python", "sas", "sql"].includes(language)) { + if (["python", "sas", "sql", "rlang", "julia"].includes(language)) { if (language === "sql") { code = `PROC SQL; ${code}; diff --git a/client/src/components/notebook/Controller.ts b/client/src/components/notebook/Controller.ts index c23bab5b7..595600ceb 100644 --- a/client/src/components/notebook/Controller.ts +++ b/client/src/components/notebook/Controller.ts @@ -11,7 +11,7 @@ export class NotebookController { readonly controllerId = "sas-notebook-controller-id"; readonly notebookType = "sas-notebook"; readonly label = "SAS Notebook"; - readonly supportedLanguages = ["sas", "sql", "python"]; + readonly supportedLanguages = ["sas", "sql", "python", "r", "julia"]; private readonly _controller: vscode.NotebookController; private _executionOrder = 0; diff --git a/client/src/components/notebook/exporters/toHTML.ts b/client/src/components/notebook/exporters/toHTML.ts index c8c910eae..c117cd130 100644 --- a/client/src/components/notebook/exporters/toHTML.ts +++ b/client/src/components/notebook/exporters/toHTML.ts @@ -16,7 +16,9 @@ import { import { readFileSync } from "fs"; import hljs from "highlight.js/lib/core"; +import julia from "highlight.js/lib/languages/julia"; import python from "highlight.js/lib/languages/python"; +import r from "highlight.js/lib/languages/r"; import sql from "highlight.js/lib/languages/sql"; import { marked } from "marked"; import path from "path"; @@ -27,7 +29,9 @@ import { includeLogInNotebookExport } from "../../utils/settings"; const templatesDir = path.resolve(__dirname, "../notebook/exporters/templates"); hljs.registerLanguage("python", python); +hljs.registerLanguage("r", r); hljs.registerLanguage("sql", sql); +hljs.registerLanguage("julia", julia); export const exportToHTML = async ( notebook: NotebookDocument, diff --git a/client/src/components/notebook/exporters/toSAS.ts b/client/src/components/notebook/exporters/toSAS.ts index 9bd136054..f9ee0cef3 100644 --- a/client/src/components/notebook/exporters/toSAS.ts +++ b/client/src/components/notebook/exporters/toSAS.ts @@ -15,8 +15,12 @@ const exportCell = (cell: NotebookCell) => { return text; case "python": return wrapPython(text); + case "r": + return wrapRlang(text); case "sql": return wrapSQL(text); + case "julia": + return wrapJulia(text); case "markdown": return `/*\n${text}\n*/`; } @@ -36,3 +40,15 @@ submit; ${code} endsubmit; run;`; + +const wrapRlang = (code: string) => `proc rlang; +submit; +${code} +endsubmit; +run;`; + +const wrapJulia = (code: string) => `proc julia; +submit; +${code} +endsubmit; +run;`; diff --git a/client/src/components/utils/SASCodeDocument.ts b/client/src/components/utils/SASCodeDocument.ts index 4ab4b054d..288b8f198 100644 --- a/client/src/components/utils/SASCodeDocument.ts +++ b/client/src/components/utils/SASCodeDocument.ts @@ -180,6 +180,22 @@ endsubmit; run;`; } + private wrapRlang(code: string) { + return `proc rlang; +submit; +${code} +endsubmit; +run;`; + } + + private wrapJulia(code: string) { + return `proc julia; +submit; +${code} +endsubmit; +run;`; + } + private insertLogStartIndicator(code: string): string { // add a comment line at the top of code, // this comment line will be used as indicator to the beginning of log related with this code @@ -198,6 +214,14 @@ ${code}`; wrapped = this.wrapPython(wrapped); } + if (this.parameters.languageId === "r") { + wrapped = this.wrapRlang(wrapped); + } + + if (this.parameters.languageId === "julia") { + wrapped = this.wrapJulia(wrapped); + } + wrapped = this.wrapCodeWithSASProgramFileName(wrapped); wrapped = this.wrapCodeWithPreambleAndPostamble(wrapped); diff --git a/client/test/components/util/SASCodeDocument.test.ts b/client/test/components/util/SASCodeDocument.test.ts index e4cef507e..5c37ba56b 100644 --- a/client/test/components/util/SASCodeDocument.test.ts +++ b/client/test/components/util/SASCodeDocument.test.ts @@ -38,6 +38,74 @@ run; assert.equal(sasCodeDoc.getWrappedCode(), expected); }); + it("wrap rlang code", () => { + const parameters: SASCodeDocumentParameters = { + languageId: "r", + code: `for (x in 1:6) { + print(x) +} +print("test")`, + selectedCode: "", + htmlStyle: "Illuminate", + outputHtml: true, + uuid: "519058ad-d33b-4b5c-9d23-4cc8d6ffb163", + checkKeyword: async () => false, + }; + + const sasCodeDoc = new SASCodeDocument(parameters); + + const expected = `/** LOG_START_INDICATOR **/ +title;footnote;ods _all_ close; +ods graphics on; +ods html5(id=vscode) style=Illuminate options(bitmap_mode='inline' svg_mode='inline') body="519058ad-d33b-4b5c-9d23-4cc8d6ffb163.htm"; +proc rlang; +submit; +for (x in 1:6) { + print(x) +} +print("test") +endsubmit; +run; +;*';*";*/;run;quit;ods html5(id=vscode) close; +`; + + assert.equal(sasCodeDoc.getWrappedCode(), expected); + }); + + it("wrap julia code", () => { + const parameters: SASCodeDocumentParameters = { + languageId: "julia", + code: `# Julia code example +println("Hello from Julia!") +x = [1, 2, 3, 4, 5] +mean_val = sum(x) / length(x)`, + selectedCode: "", + htmlStyle: "Illuminate", + outputHtml: true, + uuid: "519058ad-d33b-4b5c-9d23-4cc8d6ffb163", + checkKeyword: async () => false, + }; + + const sasCodeDoc = new SASCodeDocument(parameters); + + const expected = `/** LOG_START_INDICATOR **/ +title;footnote;ods _all_ close; +ods graphics on; +ods html5(id=vscode) style=Illuminate options(bitmap_mode='inline' svg_mode='inline') body="519058ad-d33b-4b5c-9d23-4cc8d6ffb163.htm"; +proc julia; +submit; +# Julia code example +println("Hello from Julia!") +x = [1, 2, 3, 4, 5] +mean_val = sum(x) / length(x) +endsubmit; +run; +;*';*";*/;run;quit;ods html5(id=vscode) close; +`; + + assert.equal(sasCodeDoc.getWrappedCode(), expected); + }); + it("wrap sql code", () => { const parameters: SASCodeDocumentParameters = { languageId: "sql", diff --git a/client/testFixture/formatter/expected.sas b/client/testFixture/formatter/expected.sas index 7cb36d0c7..1e8fd41a4 100644 --- a/client/testFixture/formatter/expected.sas +++ b/client/testFixture/formatter/expected.sas @@ -46,6 +46,26 @@ def my_function(): endsubmit; run; +proc rlang; +submit; +# Reference to variable defined in previous PROC RLANG call +print(paste("x =", x)) +my_function <- function() { + print("Inside the proc step") +} +endsubmit; +run; + +proc julia; +submit; +# Reference to variable defined in previous PROC JULIA call +println("x = ", x) +function my_function() + println("Inside the proc step") +end +endsubmit; +run; + proc lua; submit; local dsid = sas.open("sashelp.company") -- open for input @@ -131,6 +151,28 @@ print('first statement after for loop') endinteractive; run; +proc rlang; +submit; +fruits <- c("apple", "banana", "cherry") +for (x in fruits) { + print(x) +} + +print('first statement after for loop') +endsubmit; +run; + +proc julia; +submit; +fruits = ["apple", "banana", "cherry"] +for x in fruits + println(x) +end + +println("first statement after for loop") +endsubmit; +run; + proc lua; submit; diff --git a/client/testFixture/formatter/unformatted.sas b/client/testFixture/formatter/unformatted.sas index 923c7adaf..f289e3c6a 100644 --- a/client/testFixture/formatter/unformatted.sas +++ b/client/testFixture/formatter/unformatted.sas @@ -40,6 +40,24 @@ def my_function(): print("Inside the proc step") endsubmit; run; +proc rlang; +submit; +# Reference to variable defined in previous PROC RLANG call +print(paste("x =", x)) +my_function <- function() { + print("Inside the proc step") +} +endsubmit; +run; +proc julia; +submit; +# Reference to variable defined in previous PROC JULIA call +println("x = ", x) +function my_function() + println("Inside the proc step") +end +endsubmit; +run; proc lua; submit; local dsid = sas.open("sashelp.company") -- open for input @@ -126,6 +144,28 @@ print('first statement after for loop') endinteractive; run; +proc rlang; +submit; +fruits <- c("apple", "banana", "cherry") +for (x in fruits) { + print(x) +} + +print('first statement after for loop') +endsubmit; +run; + +proc julia; +submit; +fruits = ["apple", "banana", "cherry"] +for x in fruits + println(x) +end + +println("first statement after for loop") +endsubmit; +run; + proc lua; submit; diff --git a/client/testFixture/sasnb_export.sas b/client/testFixture/sasnb_export.sas index 088afec00..6af44c718 100644 --- a/client/testFixture/sasnb_export.sas +++ b/client/testFixture/sasnb_export.sas @@ -19,6 +19,40 @@ print("Result: ", a*10 + b) endsubmit; run; +/* +## R Code + +This is some R code +*/ + +/* +This is a separate note in **Markdown** format. +*/ + +proc rlang; +submit; +die <- 1:6 +paste("Die Maths: ", die[3]*4 + die[6]) +endsubmit; +run; + +/* +## Julia Code + +This is some Julia code +*/ + +/* +This is a separate note in **Markdown** format. +*/ + +proc julia; +submit; +dice=1:6 +println("Dice Maths: ", dice[3]*4 + dice[6]) +endsubmit; +run; + /* ## SAS Code */ diff --git a/client/testFixture/sasnb_export.sasnb b/client/testFixture/sasnb_export.sasnb index 7ed0c4ad8..3aa791dfe 100644 --- a/client/testFixture/sasnb_export.sasnb +++ b/client/testFixture/sasnb_export.sasnb @@ -1 +1 @@ -[{"kind":1,"language":"markdown","value":"# Notebook to SAS Test","outputs":[]},{"kind":1,"language":"markdown","value":"## Python Code\n\nThis is some Python code","outputs":[]},{"kind":1,"language":"markdown","value":"This is a separate note in **Markdown** format.","outputs":[]},{"kind":2,"language":"python","value":"a, b = 4, 2\nprint(\"Result: \", a*10 + b)","outputs":[{"items":[{"data":"[\n\t{\n\t\t\"line\": \"44 ods html5;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Writing HTML5 Body file: sashtml4.htm\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"45 proc python;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"46 submit\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Resuming Python state from previous PROC PYTHON invocation.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"46 ! ;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"47 a, b = 4, 2\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"48 print(\\\"Result: \\\", a*10 + b)\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"49 endsubmit;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"50 run;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \">>>\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"Result: 42\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \">>> \",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: PROCEDURE PYTHON used (Total process time):\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" real time 0.00 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" cpu time 0.00 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" \",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"51 ;run;quit;ods html5 close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t}\n]","mime":"application/vnd.sas.compute.log.lines"}]}]},{"kind":1,"language":"markdown","value":"## SAS Code","outputs":[]},{"kind":2,"language":"sas","value":"data work.prdsale;\n\tset sashelp.PRDSALE;\nrun;\n\nproc means data=work.prdsale;\nrun;","outputs":[{"items":[{"data":"\n\n\n\n\nSAS Output\n\n\n\n
\n
\n

The SAS System

\n
\n
\n

The MEANS Procedure

\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
VariableLabelNMeanStd DevMinimumMaximum
\n
\n
ACTUAL
\n
PREDICT
\n
QUARTER
\n
YEAR
\n
MONTH
\n
\n
\n
\n
Actual Sales
\n
Predicted Sales
\n
Quarter
\n
Year
\n
Month
\n
\n
\n
\n
1440
\n
1440
\n
1440
\n
1440
\n
1440
\n
\n
\n
\n
507.1784722
\n
490.4826389
\n
2.5000000
\n
1993.50
\n
12403.00
\n
\n
\n
\n
287.0313065
\n
285.7667904
\n
1.1184224
\n
0.5001737
\n
210.6291578
\n
\n
\n
\n
3.0000000
\n
0
\n
1.0000000
\n
1993.00
\n
12054.00
\n
\n
\n
\n
1000.00
\n
1000.00
\n
4.0000000
\n
1994.00
\n
12753.00
\n
\n
\n
\n
\n\n\n","mime":"application/vnd.sas.ods.html5"},{"data":"[\n\t{\n\t\t\"line\": \"16 ods html5;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Writing HTML5 Body file: sashtml1.htm\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"17 data work.prdsale;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"18 \\tset sashelp.PRDSALE;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"19 run;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: There were 1440 observations read from the data set SASHELP.PRDSALE.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: The data set WORK.PRDSALE has 1440 observations and 10 variables.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: DATA statement used (Total process time):\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" real time 0.00 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" cpu time 0.01 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" \",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"20 \",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"21 proc means data=work.prdsale;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"22 run;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: There were 1440 observations read from the data set WORK.PRDSALE.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: The PROCEDURE MEANS printed page 1.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: PROCEDURE MEANS used (Total process time):\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" real time 0.04 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" cpu time 0.05 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" \",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"23 ;run;quit;ods html5 close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t}\n]","mime":"application/vnd.sas.compute.log.lines"}]}]},{"kind":1,"language":"markdown","value":"## SQL Code","outputs":[]},{"kind":2,"language":"sql","value":"CREATE TABLE WORK.QUERY_PRDSALE AS\n SELECT\n (t1.COUNTRY) LABEL='Country' FORMAT=$CHAR10.,\n (SUM(t1.ACTUAL)) FORMAT=DOLLAR12.2 LENGTH=8 AS SUM_ACTUAL\n FROM\n WORK.PRDSALE t1\n GROUP BY\n t1.COUNTRY","outputs":[{"items":[{"data":"[\n\t{\n\t\t\"line\": \"24 ods html5;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Writing HTML5 Body file: sashtml2.htm\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"25 proc sql;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"26 CREATE TABLE WORK.QUERY_PRDSALE AS\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"27 SELECT\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"28 (t1.COUNTRY) LABEL='Country' FORMAT=$CHAR10.,\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"29 (SUM(t1.ACTUAL)) FORMAT=DOLLAR12.2 LENGTH=8 AS SUM_ACTUAL\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"30 FROM\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"31 WORK.PRDSALE t1\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"32 GROUP BY\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"33 t1.COUNTRY\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"34 ;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Table WORK.QUERY_PRDSALE created, with 3 rows and 2 columns.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"34 ! quit;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: PROCEDURE SQL used (Total process time):\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" real time 0.00 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" cpu time 0.00 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" \",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"6 The SAS System Monday, August 21, 2023 02:56:00 PM\",\n\t\t\"type\": \"title\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"title\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"35 ;run;quit;ods html5 close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t}\n]","mime":"application/vnd.sas.compute.log.lines"}]}]},{"kind":1,"language":"markdown","value":"A last comment in Markdown at the end of the document","outputs":[]},{"kind":1,"language":"markdown","value":"","outputs":[]}] \ No newline at end of file +[{"kind":1,"language":"markdown","value":"# Notebook to SAS Test","outputs":[]},{"kind":1,"language":"markdown","value":"## Python Code\n\nThis is some Python code","outputs":[]},{"kind":1,"language":"markdown","value":"This is a separate note in **Markdown** format.","outputs":[]},{"kind":2,"language":"python","value":"a, b = 4, 2\nprint(\"Result: \", a*10 + b)","outputs":[{"items":[{"data":"[\n\t{\n\t\t\"line\": \"14 /** LOG_START_INDICATOR **/\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"15 title;footnote;ods _all_ close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"16 ods graphics on;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"17 ods html5(id=vscode) style=Ignite options(bitmap_mode='inline' svg_mode='inline');\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Writing HTML5(VSCODE) Body file: sashtml1.htm\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"18 %let _SASPROGRAMFILE = %nrquote(%nrstr(/Users/elreid/personalGit/vscode-sas-extension/client/testFixture/sasnb_export.sasnb));\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"19 proc python;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"20 submit\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Python initialized.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"Python 3.11.10 (main, Nov 30 2025, 14:30:37) [GCC 11.5.0 20240719 (Red Hat 11.5.0-11)] on linux\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"Type \\\"help\\\", \\\"copyright\\\", \\\"credits\\\" or \\\"license\\\" for more information.\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \">>>\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \">>> \",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"20 ! ;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"21 a, b = 4, 2\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"22 print(\\\"Result: \\\", a*10 + b)\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"23 endsubmit;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"24 run;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \">>>\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"Result: 42\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \">>> \",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: PROCEDURE PYTHON used (Total process time):\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" real time 1.99 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" cpu time 0.08 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" \",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"25 ;*';*\\\";*/;run;quit;ods html5(id=vscode) close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"26 \",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t}\n]","mime":"application/vnd.sas.compute.log.lines"}]}]},{"kind":1,"language":"markdown","value":"## R Code\n\nThis is some R code","outputs":[]},{"kind":1,"language":"markdown","value":"This is a separate note in **Markdown** format.","outputs":[]},{"kind":2,"language":"r","value":"die <- 1:6\npaste(\"Die Maths: \", die[3]*4 + die[6])","outputs":[{"items":[{"data":"[\n\t{\n\t\t\"line\": \"27 /** LOG_START_INDICATOR **/\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"28 title;footnote;ods _all_ close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"29 ods graphics on;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"30 ods html5(id=vscode) style=Ignite options(bitmap_mode='inline' svg_mode='inline');\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Writing HTML5(VSCODE) Body file: sashtml2.htm\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"31 %let _SASPROGRAMFILE = %nrquote(%nrstr(/Users/elreid/personalGit/vscode-sas-extension/client/testFixture/sasnb_export.sasnb));\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"32 proc rlang;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"33 submit\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Resuming Python state from previous PROC PYTHON invocation.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"33 ! ;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"34 die <- 1:6\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"35 paste(\\\"Die Maths: \\\", die[3]*4 + die[6])\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"36 endsubmit;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"37 run;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"[1] \\\"Die Maths: 18\\\"\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: PROCEDURE RLANG used (Total process time):\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" real time 0.00 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" cpu time 0.00 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" \",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"38 ;*';*\\\";*/;run;quit;ods html5(id=vscode) close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"39 \",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t}\n]","mime":"application/vnd.sas.compute.log.lines"}]}]},{"kind":1,"language":"markdown","value":"## Julia Code\n\nThis is some Julia code","outputs":[]},{"kind":1,"language":"markdown","value":"This is a separate note in **Markdown** format.","outputs":[]},{"kind":2,"language":"julia","value":"dice=1:6\nprintln(\"Dice Maths: \", dice[3]*4 + dice[6])","outputs":[]},{"kind":1,"language":"markdown","value":"## SAS Code","outputs":[]},{"kind":2,"language":"sas","value":"data work.prdsale;\n\tset sashelp.PRDSALE;\nrun;\n\nproc means data=work.prdsale;\nrun;","outputs":[{"items":[{"data":"\n\n\n\n\nSAS Output\n\n\n\n
\n
\n

The MEANS Procedure

\n
\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
VariableLabelNMeanStd DevMinimumMaximum
\n
\n
ACTUAL
\n
PREDICT
\n
QUARTER
\n
YEAR
\n
MONTH
\n
\n
\n
\n
Actual Sales
\n
Predicted Sales
\n
Quarter
\n
Year
\n
Month
\n
\n
\n
\n
1440
\n
1440
\n
1440
\n
1440
\n
1440
\n
\n
\n
\n
507.1784722
\n
490.4826389
\n
2.5000000
\n
1993.50
\n
12403.00
\n
\n
\n
\n
287.0313065
\n
285.7667904
\n
1.1184224
\n
0.5001737
\n
210.6291578
\n
\n
\n
\n
3.0000000
\n
0
\n
1.0000000
\n
1993.00
\n
12054.00
\n
\n
\n
\n
1000.00
\n
1000.00
\n
4.0000000
\n
1994.00
\n
12753.00
\n
\n
\n
\n
\n\n\n","mime":"application/vnd.sas.ods.html5"},{"data":"[\n\t{\n\t\t\"line\": \"40 /** LOG_START_INDICATOR **/\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"3 The SAS System Friday, 5 December 2025 14:39:00\",\n\t\t\"type\": \"title\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"title\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"41 title;footnote;ods _all_ close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"42 ods graphics on;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"43 ods html5(id=vscode) style=Ignite options(bitmap_mode='inline' svg_mode='inline');\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Writing HTML5(VSCODE) Body file: sashtml3.htm\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"44 %let _SASPROGRAMFILE = %nrquote(%nrstr(/Users/elreid/personalGit/vscode-sas-extension/client/testFixture/sasnb_export.sasnb));\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"45 data work.prdsale;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"46 \\tset sashelp.PRDSALE;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"47 run;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: There were 1440 observations read from the data set SASHELP.PRDSALE.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: The data set WORK.PRDSALE has 1440 observations and 10 variables.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: DATA statement used (Total process time):\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" real time 0.00 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" cpu time 0.02 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" \",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"48 \",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"49 proc means data=work.prdsale;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"50 run;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: There were 1440 observations read from the data set WORK.PRDSALE.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: PROCEDURE MEANS used (Total process time):\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" real time 0.05 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" cpu time 0.05 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" \",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"51 ;*';*\\\";*/;run;quit;ods html5(id=vscode) close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"52 \",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t}\n]","mime":"application/vnd.sas.compute.log.lines"}]}]},{"kind":1,"language":"markdown","value":"## SQL Code","outputs":[]},{"kind":2,"language":"sql","value":"CREATE TABLE WORK.QUERY_PRDSALE AS\n SELECT\n (t1.COUNTRY) LABEL='Country' FORMAT=$CHAR10.,\n (SUM(t1.ACTUAL)) FORMAT=DOLLAR12.2 LENGTH=8 AS SUM_ACTUAL\n FROM\n WORK.PRDSALE t1\n GROUP BY\n t1.COUNTRY","outputs":[{"items":[{"data":"[\n\t{\n\t\t\"line\": \"53 /** LOG_START_INDICATOR **/\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"54 title;footnote;ods _all_ close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"55 ods graphics on;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"56 ods html5(id=vscode) style=Ignite options(bitmap_mode='inline' svg_mode='inline');\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Writing HTML5(VSCODE) Body file: sashtml4.htm\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"57 %let _SASPROGRAMFILE = %nrquote(%nrstr(/Users/elreid/personalGit/vscode-sas-extension/client/testFixture/sasnb_export.sasnb));\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"58 proc sql;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"59 CREATE TABLE WORK.QUERY_PRDSALE AS\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"60 SELECT\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"61 (t1.COUNTRY) LABEL='Country' FORMAT=$CHAR10.,\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"62 (SUM(t1.ACTUAL)) FORMAT=DOLLAR12.2 LENGTH=8 AS SUM_ACTUAL\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"63 FROM\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"64 WORK.PRDSALE t1\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"65 GROUP BY\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"66 t1.COUNTRY\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"67 ;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: Table WORK.QUERY_PRDSALE created, with 3 rows and 2 columns.\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"normal\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"67 ! quit;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"NOTE: PROCEDURE SQL used (Total process time):\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" real time 0.01 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" cpu time 0.01 seconds\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \" \",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"68 ;*';*\\\";*/;run;quit;ods html5(id=vscode) close;\",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"69 \",\n\t\t\"type\": \"source\",\n\t\t\"version\": 1\n\t},\n\t{\n\t\t\"line\": \"\",\n\t\t\"type\": \"note\",\n\t\t\"version\": 1\n\t}\n]","mime":"application/vnd.sas.compute.log.lines"}]}]},{"kind":1,"language":"markdown","value":"A last comment in Markdown at the end of the document","outputs":[]},{"kind":1,"language":"markdown","value":"","outputs":[]}] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 95217ee7c..2d726c809 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sas-lsp", - "version": "1.17.0", + "version": "1.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sas-lsp", - "version": "1.17.0", + "version": "1.18.0", "hasInstallScript": true, "license": "Apache-2.0", "devDependencies": { @@ -200,6 +200,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1557,6 +1558,7 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1633,6 +1635,7 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -2157,6 +2160,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2519,6 +2523,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3410,6 +3415,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5731,6 +5737,7 @@ "integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6075,6 +6082,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6618,6 +6626,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6942,6 +6951,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7052,6 +7062,7 @@ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -7100,6 +7111,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -7428,6 +7440,7 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index e0af38778..b42bd0485 100644 --- a/package.json +++ b/package.json @@ -1296,7 +1296,9 @@ "embeddedLanguages": { "source.python": "python", "source.lua": "lua", - "source.sql": "sql" + "source.sql": "sql", + "source.r": "r", + "source.julia": "julia" } } ], diff --git a/server/src/sas/CodeZoneManager.ts b/server/src/sas/CodeZoneManager.ts index 08ed4f0e5..bf9c9a9c3 100644 --- a/server/src/sas/CodeZoneManager.ts +++ b/server/src/sas/CodeZoneManager.ts @@ -1132,7 +1132,7 @@ export class CodeZoneManager { this._getFullStmtName(context, this._procName, stmt); const zone = this._stmtEx(context, stmt); type = zone.type; - if (["PYTHON", "LUA"].includes(this._procName)) { + if (["PYTHON", "LUA", "RLANG", "JULIA"].includes(this._procName)) { if (!this._embeddedCodeStarted) { if (["SUBMIT", "INTERACTIVE", "I"].includes(stmt.text)) { this._embeddedCodeStarted = true; diff --git a/server/src/sas/Lexer.ts b/server/src/sas/Lexer.ts index a3f7ed182..89d3bbc5e 100644 --- a/server/src/sas/Lexer.ts +++ b/server/src/sas/Lexer.ts @@ -86,6 +86,12 @@ enum EmbeddedLangState { PROC_LUA_DEF, PROC_LUA_SUBMIT_OR_INTERACTIVE, PROC_LUA_CODE, + PROC_RLANG_DEF, + PROC_RLANG_SUBMIT_OR_INTERACTIVE, + PROC_RLANG_CODE, + PROC_JULIA_DEF, + PROC_JULIA_SUBMIT_OR_INTERACTIVE, + PROC_JULIA_CODE, } export class Lexer { start = { line: 0, column: 0 }; @@ -290,6 +296,10 @@ export class Lexer { ) { if (token.type === "text" && token.text === "PYTHON") { this.context.embeddedLangState = EmbeddedLangState.PROC_PYTHON_DEF; + } else if (token.type === "text" && token.text === "RLANG") { + this.context.embeddedLangState = EmbeddedLangState.PROC_RLANG_DEF; + } else if (token.type === "text" && token.text === "JULIA") { + this.context.embeddedLangState = EmbeddedLangState.PROC_JULIA_DEF; } else if (token.type === "text" && token.text === "LUA") { this.context.embeddedLangState = EmbeddedLangState.PROC_LUA_DEF; } @@ -324,6 +334,34 @@ export class Lexer { } break SWITCH; } + case EmbeddedLangState.PROC_RLANG_DEF: { + token = this._readToken(); + if (!token) { + break SWITCH; + } + if ( + token.type === "text" && + ["SUBMIT", "INTERACTIVE", "I"].includes(token.text) + ) { + this.context.embeddedLangState = + EmbeddedLangState.PROC_RLANG_SUBMIT_OR_INTERACTIVE; + } + break SWITCH; + } + case EmbeddedLangState.PROC_JULIA_DEF: { + token = this._readToken(); + if (!token) { + break SWITCH; + } + if ( + token.type === "text" && + ["SUBMIT", "INTERACTIVE", "I"].includes(token.text) + ) { + this.context.embeddedLangState = + EmbeddedLangState.PROC_JULIA_SUBMIT_OR_INTERACTIVE; + } + break SWITCH; + } case EmbeddedLangState.PROC_PYTHON_SUBMIT_OR_INTERACTIVE: { token = this._readToken(); if (!token) { @@ -386,6 +424,132 @@ export class Lexer { token = this._foundEmbeddedCodeToken(this.curr); break SWITCH; } + case EmbeddedLangState.PROC_RLANG_SUBMIT_OR_INTERACTIVE: { + token = this._readToken(); + if (!token) { + break SWITCH; + } + if (token.type === "sep" && token.text === ";") { + this.context.embeddedLangState = EmbeddedLangState.PROC_RLANG_CODE; + } + break SWITCH; + } + case EmbeddedLangState.PROC_RLANG_CODE: { + // R doesn't have multi-line string delimiters like Python's triple quotes + for ( + let line = this.curr.line; + line < this.model.getLineCount(); + line++ + ) { + const lineContent = this._readEmbeddedCodeLine(this.curr, line); + let pos = 0; + let match; + do { + if (match) { + pos += match.index + match[0].length; + } + const stringReg = /("[^"]*?("|$))|('[^']*?('|$))/; + const commentReg = /#.*$/; + const secReg = + /(\b((endsubmit|endinteractive)(\s+|\/\*.*?\*\/)*;|(data|proc|%macro)\b[^'";]*;))/; + match = new RegExp( + `${stringReg.source}|${commentReg.source}|${secReg.source}`, + "m", + ).exec(lineContent.substring(pos)); + if (match) { + const matchedText = match[0]; + if (/^('|"|#)/.test(matchedText)) { + // do nothing to skip string and single line comment + } else { + token = this._foundEmbeddedCodeToken(this.curr, { + line: line, + column: pos + match.index, + }); + break SWITCH; + } + } + } while (match); + } + token = this._foundEmbeddedCodeToken(this.curr); + break SWITCH; + } + case EmbeddedLangState.PROC_JULIA_SUBMIT_OR_INTERACTIVE: { + token = this._readToken(); + if (!token) { + break SWITCH; + } + if (token.type === "sep" && token.text === ";") { + this.context.embeddedLangState = EmbeddedLangState.PROC_JULIA_CODE; + } + break SWITCH; + } + case EmbeddedLangState.PROC_JULIA_CODE: { + // Julia supports triple-quoted strings and nested multi-line comments + let multiLineStrState: false | '"""' = false; + let multiLineCommentDepth = 0; + for ( + let line = this.curr.line; + line < this.model.getLineCount(); + line++ + ) { + const lineContent = this._readEmbeddedCodeLine(this.curr, line); + let pos = 0; + let match; + do { + if (match) { + pos += match.index + match[0].length; + } + if (multiLineCommentDepth > 0) { + // Inside multi-line comment #= ... =# + match = /#=|=#/.exec(lineContent.substring(pos)); + if (match) { + if (match[0] === "#=") { + multiLineCommentDepth++; + } else if (match[0] === "=#") { + multiLineCommentDepth--; + } + } + } else if (multiLineStrState) { + // Inside triple-quoted string + match = /"""/.exec(lineContent.substring(pos)); + if (match) { + multiLineStrState = false; + } + } else { + // Looking for strings, comments, or SAS keywords + const stringReg = /"""|("[^"\\\n]*(\\.[^"\\\n]*)*")/; + const commentReg = /#=|#.*$/; + const secReg = + /(\b((endsubmit|endinteractive)(\s+|\/\*.*?\*\/)*;|(data|proc|%macro)\b[^'";]*;))/; + match = new RegExp( + `${stringReg.source}|${commentReg.source}|${secReg.source}`, + "m", + ).exec(lineContent.substring(pos)); + if (match) { + const matchedText = match[0]; + if (matchedText === '"""') { + multiLineStrState = '"""'; + } else if (matchedText === "#=") { + multiLineCommentDepth = 1; + } else if ( + matchedText.startsWith('"') || + matchedText.startsWith("#") + ) { + // do nothing to skip string and single line comment + } else { + token = this._foundEmbeddedCodeToken(this.curr, { + line: line, + column: pos + match.index, + }); + break SWITCH; + } + } + } + } while (match); + } + token = this._foundEmbeddedCodeToken(this.curr); + break SWITCH; + } case EmbeddedLangState.PROC_LUA_SUBMIT_OR_INTERACTIVE: { token = this._readToken(); if (!token) { diff --git a/server/src/sas/LexerEx.ts b/server/src/sas/LexerEx.ts index 8ca905437..feed029ff 100644 --- a/server/src/sas/LexerEx.ts +++ b/server/src/sas/LexerEx.ts @@ -3101,7 +3101,12 @@ export class LexerEx { this.setKeyword_(token, true); generalProcStmt = false; } - } else if (procName === "LUA" || procName === "PYTHON") { + } else if ( + procName === "LUA" || + procName === "PYTHON" || + procName === "RLANG" || + procName === "JULIA" + ) { if (["SUBMIT", "INTERACTIVE", "I"].includes(word)) { const next = this.prefetch_({ pos: 1 }); if (next && next.text === ";" && next.type === "sep") { diff --git a/server/src/sas/formatter/parser.ts b/server/src/sas/formatter/parser.ts index 07f4e6cba..bc3899fe0 100644 --- a/server/src/sas/formatter/parser.ts +++ b/server/src/sas/formatter/parser.ts @@ -124,7 +124,7 @@ const preserveProcs = ( token: Token, model: Model, ) => { - // should not format python/lua, treat it as raw data + // should not format python/R/julia/lua, treat it as raw data const lastStatement = region.children.length >= 2 && region.children[region.children.length - 1].children; @@ -135,7 +135,7 @@ const preserveProcs = ( region.children[0].children.length > 0 && lastStatement.length > 1 && "text" in region.children[0].children[1] && - /^(python|lua)$/i.test(region.children[0].children[1].text) && + /^(python|lua|rlang|julia)$/i.test(region.children[0].children[1].text) && "text" in lastStatement[0] && /^(submit|interactive|i)$/i.test(lastStatement[0].text) ) { @@ -286,7 +286,7 @@ export const getParser = const node = tokens[i]; let parent = parents.length ? parents[parents.length - 1] : root; - //#region --- Preserve Python/Lua + //#region --- Preserve Python/R/Julia/Lua if (region && region.block) { preserveProc = preserveProcs(preserveProc, region, node, model); if (preserveProc === 0 && i === tokens.length - 1) { diff --git a/server/test/embedded_lang/embedded_lang.test.ts b/server/test/embedded_lang/embedded_lang.test.ts index b8c1b3ed7..c2bc05b1e 100644 --- a/server/test/embedded_lang/embedded_lang.test.ts +++ b/server/test/embedded_lang/embedded_lang.test.ts @@ -64,4 +64,34 @@ describe("Test code zone for embedded language", () => { assert.equal(zoneList[4], CodeZoneManager.ZONE_TYPE.EMBEDDED_LANG); assert.equal(zoneList[6], CodeZoneManager.ZONE_TYPE.PROC_STMT); }); + + it("proc rlang", () => { + const doc = openDoc("server/testFixture/embedded_lang/proc_rlang.sas"); + const languageServer = new LanguageServiceProvider(doc); + const codeZoneManager = languageServer.getCodeZoneManager(); + + const zoneList = []; + for (let i = 0; i < doc.lineCount; i++) { + zoneList.push(codeZoneManager.getCurrentZone(i, 1)); + } + assert.equal(zoneList[1], CodeZoneManager.ZONE_TYPE.PROC_STMT); + assert.equal(zoneList[2], CodeZoneManager.ZONE_TYPE.EMBEDDED_LANG); + assert.equal(zoneList[4], CodeZoneManager.ZONE_TYPE.EMBEDDED_LANG); + assert.equal(zoneList[6], CodeZoneManager.ZONE_TYPE.PROC_STMT); + }); + + it("proc julia", () => { + const doc = openDoc("server/testFixture/embedded_lang/proc_julia.sas"); + const languageServer = new LanguageServiceProvider(doc); + const codeZoneManager = languageServer.getCodeZoneManager(); + + const zoneList = []; + for (let i = 0; i < doc.lineCount; i++) { + zoneList.push(codeZoneManager.getCurrentZone(i, 1)); + } + assert.equal(zoneList[1], CodeZoneManager.ZONE_TYPE.PROC_STMT); + assert.equal(zoneList[2], CodeZoneManager.ZONE_TYPE.EMBEDDED_LANG); + assert.equal(zoneList[4], CodeZoneManager.ZONE_TYPE.EMBEDDED_LANG); + assert.equal(zoneList[6], CodeZoneManager.ZONE_TYPE.PROC_STMT); + }); }); diff --git a/server/testFixture/embedded_lang/proc_julia.sas b/server/testFixture/embedded_lang/proc_julia.sas new file mode 100644 index 000000000..caaf30ba3 --- /dev/null +++ b/server/testFixture/embedded_lang/proc_julia.sas @@ -0,0 +1,8 @@ +proc julia; +submit; + for x in 1:6 + print(x) + end + print("first statement after for loop") +endsubmit; +run; \ No newline at end of file diff --git a/server/testFixture/embedded_lang/proc_rlang.sas b/server/testFixture/embedded_lang/proc_rlang.sas new file mode 100644 index 000000000..da286d1e4 --- /dev/null +++ b/server/testFixture/embedded_lang/proc_rlang.sas @@ -0,0 +1,8 @@ +proc rlang; +submit; + for (x in 1:6) { + print(x) + } + print("first statement after for loop") +endsubmit; +run; \ No newline at end of file diff --git a/website/docs/Features/sasNotebook.md b/website/docs/Features/sasNotebook.md index c2e7afb8d..01066361d 100644 --- a/website/docs/Features/sasNotebook.md +++ b/website/docs/Features/sasNotebook.md @@ -25,7 +25,7 @@ To export your SAS Notebook to other formats, click the **More Actions** (`...`) ### SAS -PYTHON and SQL code cells will be wrapped with PROC PYTHON/SQL respectively to be executed on SAS. Markdown cells will be converted to block comments. +PYTHON, R, and SQL code cells will be wrapped with PROC PYTHON/RLANG/SQL respectively to be executed on SAS. Markdown cells will be converted to block comments. ### HTML diff --git a/website/docs/README.md b/website/docs/README.md index e54c39d6e..8955ff2ef 100644 --- a/website/docs/README.md +++ b/website/docs/README.md @@ -16,4 +16,4 @@ The SAS extension includes the following features: - Access to SAS Content and libraries -- Ability to create notebooks for SAS, SQL, Python, and other languages +- Ability to create notebooks for SAS, SQL, Python, R, and other languages