diff --git a/src/execute/julia.ts b/src/execute/julia.ts index a852b6e6e0c..f8af297f9bd 100644 --- a/src/execute/julia.ts +++ b/src/execute/julia.ts @@ -3,7 +3,11 @@ import { join } from "../deno_ral/path.ts"; import { MappedString, mappedStringFromFile } from "../core/mapped-text.ts"; import { partitionMarkdown } from "../core/pandoc/pandoc-partition.ts"; import { readYamlFromMarkdown } from "../core/yaml.ts"; -import { asMappedString } from "../core/lib/mapped-text.ts"; +import { + asMappedString, + mappedIndexToLineCol, + mappedLines, +} from "../core/lib/mapped-text.ts"; import { ProjectContext } from "../project/types.ts"; import { DependenciesOptions, @@ -52,6 +56,13 @@ import { isJupyterPercentScript, markdownFromJupyterPercentScript, } from "./jupyter/percent.ts"; +import { resolve } from "path"; + +export interface SourceRange { + lines: [number, number]; + file?: string; + sourceLines?: [number, number]; +} export interface JuliaExecuteOptions extends ExecuteOptions { oneShot: boolean; // if true, the file's worker process is closed before and after running @@ -578,6 +589,64 @@ function getConsoleColumns(): number | null { } } +function buildSourceRanges(markdown: MappedString): Array { + const lines = mappedLines(markdown); + const sourceRanges: Array = []; + let currentRange: SourceRange | null = null; + + lines.forEach((line, index) => { + // Get mapping info directly from the line's MappedString + const mapResult = line.map(0, true); + if (mapResult) { + const { originalString } = mapResult; + const lineColFunc = mappedIndexToLineCol(originalString); + const lineCol = lineColFunc(mapResult.index); + const fileName = originalString.fileName + ? resolve(originalString.fileName) // resolve to absolute path using cwd + : undefined; + const sourceLineNum = lineCol.line; + + // Check if this line continues the current range + if ( + currentRange && + currentRange.file === fileName && + fileName !== undefined && + currentRange.sourceLines && + currentRange.sourceLines[1] === sourceLineNum + ) { + // Extend current range + currentRange.lines[1] = index + 1; // +1 because lines are 1-indexed + currentRange.sourceLines[1] = sourceLineNum + 1; + } else { + // Start new range + if (currentRange) { + sourceRanges.push(currentRange); + } + currentRange = { + lines: [index + 1, index + 1], // +1 because lines are 1-indexed + }; + if (fileName !== undefined) { + currentRange.file = fileName; + currentRange.sourceLines = [sourceLineNum + 1, sourceLineNum + 1]; + } + } + } else { + // No mapping available - treat as separate range + if (currentRange) { + sourceRanges.push(currentRange); + currentRange = null; + } + } + }); + + // Don't forget the last range + if (currentRange) { + sourceRanges.push(currentRange); + } + + return sourceRanges; +} + async function executeJulia( options: JuliaExecuteOptions, ): Promise { @@ -600,9 +669,12 @@ async function executeJulia( ); } } + + const sourceRanges = buildSourceRanges(options.target.markdown); + const response = await writeJuliaCommand( conn, - { type: "run", content: { file, options } }, + { type: "run", content: { file, options, sourceRanges } }, transportOptions.key, options, (update: ProgressUpdate) => { @@ -643,7 +715,14 @@ interface ProgressUpdate { type empty = Record; type ServerCommand = - | { type: "run"; content: { file: string; options: JuliaExecuteOptions } } + | { + type: "run"; + content: { + file: string; + options: JuliaExecuteOptions; + sourceRanges: Array; + }; + } | { type: "close"; content: { file: string } } | { type: "forceclose"; content: { file: string } } | { type: "isopen"; content: { file: string } } @@ -854,7 +933,9 @@ function populateJuliaEngineCommand(command: Command) { "Get status information on the currently running Julia server process.", ).action(logStatus) .command("kill", "Kill server") - .description("Kill the control server if it is currently running. This will also kill all notebook worker processes.") + .description( + "Kill the control server if it is currently running. This will also kill all notebook worker processes.", + ) .action(killJuliaServer) .command("log", "Print julia server log") .description( diff --git a/src/resources/julia/Project.toml b/src/resources/julia/Project.toml index 08c36b12cfb..1e6ef8fceab 100644 --- a/src/resources/julia/Project.toml +++ b/src/resources/julia/Project.toml @@ -1,5 +1,8 @@ [deps] QuartoNotebookRunner = "4c0109c6-14e9-4c88-93f0-2b974d3468f4" -[compat] -QuartoNotebookRunner = "=0.17.3" +# [compat] +# QuartoNotebookRunner = "=0.17.3" + +[sources] +QuartoNotebookRunner = {url = "https://github.com/PumasAI/QuartoNotebookRunner.jl", rev = "jk/source-ranges"} diff --git a/tests/docs/smoke-all/julia/_included.qmd b/tests/docs/smoke-all/julia/_included.qmd new file mode 100644 index 00000000000..1be592fa71f --- /dev/null +++ b/tests/docs/smoke-all/julia/_included.qmd @@ -0,0 +1,3 @@ +```{julia} +"$(@__FILE__):$(@__LINE__)" +``` \ No newline at end of file diff --git a/tests/docs/smoke-all/julia/source-ranges-test.qmd b/tests/docs/smoke-all/julia/source-ranges-test.qmd new file mode 100644 index 00000000000..a1947ac784d --- /dev/null +++ b/tests/docs/smoke-all/julia/source-ranges-test.qmd @@ -0,0 +1,16 @@ +--- +title: "Test Julia source ranges with includes" +format: markdown +engine: julia +_quarto: + tests: + markdown: + ensureFileRegexMatches: + - ['source-ranges-test\.qmd:15', '_included\.qmd:2'] +--- + +{{< include _included.qmd >}} + +```{julia} +"$(@__FILE__):$(@__LINE__)" +```