From d9a5f6d77693ea54787e2c6ad61c1d881cb6c86a Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Thu, 11 Sep 2025 16:43:25 -0700 Subject: [PATCH 1/5] fix: fix autodedenting issue --- src/Sync.js | 28 +++++++++++++++++++++++----- src/deindent.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 src/deindent.js diff --git a/src/Sync.js b/src/Sync.js index 685ca50..8490fd6 100644 --- a/src/Sync.js +++ b/src/Sync.js @@ -25,6 +25,9 @@ const progress = require("cli-progress"); const glob = require("glob"); const { type } = require("os"); +// Deindenting dependencies +const { deindentByCommonPrefix, SENSITIVE_INDENT_EXTS } = require("./deindent"); + // Convert dependency functions to return promises const writeAsync = promisify(writeFile); const unlinkAsync = promisify(unlink); @@ -56,10 +59,20 @@ class Snippet { lines.push(textline); } if (config.select !== undefined) { - const selectedLines = selectLines(config.select, this.lines, this.ext); + let selectedLines = selectLines(config.select, this.lines, this.ext); + + if (!SENSITIVE_INDENT_EXTS.has(this.ext) &&selectedLines.length) { + selectedLines = deindentByCommonPrefix(selectedLines); + } lines.push(...selectedLines); } else if(!config.startPattern && !config.endPattern ) { - lines.push(...this.lines); + let snippetLines = [...this.lines]; + + if (!SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) { + snippetLines = deindentByCommonPrefix(snippetLines); + } + + lines.push(...snippetLines); } else { // use the patterns to grab the content specified. @@ -67,10 +80,15 @@ class Snippet { const match = this.lines.join("\n").match(pattern); if (match !== null) { - let filteredLines = match[1].split("\n"); - lines.push(...filteredLines); + let snippetLines = match[1].split("\n"); + + if (!SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) { + snippetLines = deindentByCommonPrefix(snippetLines); + } + + lines.push(...snippetLines); + } } - } if (config.enable_code_block) { lines.push(markdownCodeTicks); diff --git a/src/deindent.js b/src/deindent.js new file mode 100644 index 0000000..591a478 --- /dev/null +++ b/src/deindent.js @@ -0,0 +1,31 @@ +function commonIndentPrefix(lines) { + const nonEmpty = lines.filter(l => l.trim().length > 0); + if (nonEmpty.length === 0) return ""; + + // Optionally skip lines that are just a closing token + const closingHead = /^(?:}|\]|\)|end\b)\s*$/; + const candidates = nonEmpty.filter(l => !closingHead.test(l.trim())); + const pool = candidates.length ? candidates : nonEmpty; + + // Compute common prefix of leading whitespace (tabs/spaces preserved) + const prefixes = pool.map(l => (l.match(/^[\t ]*/)[0] || "")); + let prefix = prefixes[0]; + for (let i = 1; i < prefixes.length; i++) { + let j = 0; + while (j < prefix.length && j < prefixes[i].length && prefix[j] === prefixes[i][j]) j++; + prefix = prefix.slice(0, j); + if (prefix === "") break; + } + return prefix; +} + +function deindentByCommonPrefix(lines) { + const prefix = commonIndentPrefix(lines); + if (!prefix) return lines.slice(); + const re = new RegExp("^" + prefix.replace(/[\t ]/g, m => (m === "\t" ? "\\t" : " "))); + return lines.map(l => (l.startsWith(prefix) ? l.replace(re, "") : l)); +} + +const SENSITIVE_INDENT_EXTS = new Set(['make', 'mk', 'Makefile', 'diff']); + +module.exports = { deindentByCommonPrefix, SENSITIVE_INDENT_EXTS }; From d7dc14b6842a706cabfa121a5d753a54d7889e1c Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Thu, 11 Sep 2025 17:21:38 -0700 Subject: [PATCH 2/5] fix: modify tests; add dedenting --- package-lock.json | 4 +-- src/Sync.js | 48 +++++++++++++++++--------------- src/config.js | 2 +- test/fixtures/dedent.md | 8 ++++++ test/sync.test.js | 61 +++++++++++++++++++++-------------------- 5 files changed, 69 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index ccf033e..5327674 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "snipsync", - "version": "1.10.0", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "snipsync", - "version": "1.10.0", + "version": "1.11.0", "license": "MIT", "dependencies": { "@octokit/rest": "^18.12.0", diff --git a/src/Sync.js b/src/Sync.js index 8490fd6..cde1d3f 100644 --- a/src/Sync.js +++ b/src/Sync.js @@ -15,7 +15,6 @@ const { writeEnd, } = require("./common"); const { writeFile, unlink } = require("fs"); -const dedent = require("dedent"); const path = require("path"); const arrayBuffToBuff = require("arraybuffer-to-buffer"); const anzip = require("anzip"); @@ -59,16 +58,17 @@ class Snippet { lines.push(textline); } if (config.select !== undefined) { - let selectedLines = selectLines(config.select, this.lines, this.ext); + let snippetLines = selectLines(config.select, this.lines, this.ext); - if (!SENSITIVE_INDENT_EXTS.has(this.ext) &&selectedLines.length) { - selectedLines = deindentByCommonPrefix(selectedLines); + if (config.enable_code_dedenting && !SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) { + snippetLines = deindentByCommonPrefix(snippetLines); } - lines.push(...selectedLines); + + lines.push(...snippetLines); } else if(!config.startPattern && !config.endPattern ) { let snippetLines = [...this.lines]; - if (!SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) { + if (config.enable_code_dedenting && !SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) { snippetLines = deindentByCommonPrefix(snippetLines); } @@ -82,7 +82,7 @@ class Snippet { if (match !== null) { let snippetLines = match[1].split("\n"); - if (!SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) { + if (config.enable_code_dedenting && !SENSITIVE_INDENT_EXTS.has(this.ext) && snippetLines.length) { snippetLines = deindentByCommonPrefix(snippetLines); } @@ -154,13 +154,8 @@ class File { this.lines = []; } // fileString converts the array of lines into a string - fileString(dedentCode = false) { + fileString() { let lines = `${this.lines.join("\n")}\n`; - - if (dedentCode) { - lines = dedent(lines); - } - return lines; } } @@ -485,7 +480,7 @@ class Sync { for (const file of files) { await writeAsync( file.fullpath, - file.fileString(this.config.features.enable_code_dedenting) + file.fileString() ); this.progress.increment(); } @@ -548,15 +543,24 @@ function extractWriteIDAndConfig(line) { function overwriteConfig(current, extracted) { let config = {}; - config.enable_source_link = - extracted?.enable_source_link ?? true - ? current.enable_source_link - : extracted.enable_source_link; + // use snippet override if present, otherwise use global default + if (extracted && 'enable_source_link' in extracted) { + config.enable_source_link = extracted.enable_source_link; + } else { + config.enable_source_link = current.enable_source_link; + } - config.enable_code_block = - extracted?.enable_code_block ?? true - ? current.enable_code_block - : extracted.enable_code_block; + if (extracted && 'enable_code_block' in extracted) { + config.enable_code_block = extracted.enable_code_block; + } else { + config.enable_code_block = current.enable_code_block; + } + + if (extracted && 'enable_code_dedenting' in extracted) { + config.enable_code_dedenting = extracted.enable_code_dedenting; + } else { + config.enable_code_dedenting = current.enable_code_dedenting || false; + } if (extracted?.highlightedLines ?? undefined) { config.highlights = extracted.highlightedLines; diff --git a/src/config.js b/src/config.js index 8dc41de..420e400 100644 --- a/src/config.js +++ b/src/config.js @@ -37,7 +37,7 @@ module.exports.readConfig = (logger, file="") => { // Disable code block dedenting by default if not specified if (!Object.prototype.hasOwnProperty.call(cfg.features, 'enable_code_dedenting')) { - cfg['features']['enable_code_dedenting'] = false; + cfg['features']['enable_code_dedenting'] = true; } return cfg; diff --git a/test/fixtures/dedent.md b/test/fixtures/dedent.md index c90b9ed..fee75e5 100644 --- a/test/fixtures/dedent.md +++ b/test/fixtures/dedent.md @@ -1,2 +1,10 @@ +What if there is preceding text at zero indentation? Does it handle this well? + + +For example, this paragraph starts at flush-left. + +- However, certain items should not be dedented + - For example, this list item on another level +- Another list item \ No newline at end of file diff --git a/test/sync.test.js b/test/sync.test.js index 73d2967..df66d73 100644 --- a/test/sync.test.js +++ b/test/sync.test.js @@ -158,54 +158,57 @@ test('uses regex patterns to pare down snippet inserted into a file', async() => }); -test('Do not dedent snippets when option is false', async() => { - - fs.copyFileSync(`${fixturesPath}/dedent.md`,`${testEnvPath}/dedent.md`); - - cfg.origins = [ - { owner: 'temporalio', repo: 'samples-typescript' }, - ]; +test('No dedent when option is false (snippet stays indented; other content unchanged)', async () => { + fs.copyFileSync(`${fixturesPath}/dedent.md`, `${testEnvPath}/dedent.md`); + cfg.origins = [{ owner: 'temporalio', repo: 'samples-typescript' }]; cfg.features.enable_code_dedenting = false; const synctron = new Sync(cfg, logger); await synctron.run(); - let data = fs.readFileSync(`${testEnvPath}/dedent.md`, 'utf8'); - data = data.split("\n"); + const text = fs.readFileSync(`${testEnvPath}/dedent.md`, 'utf8'); - /* - * The code will start on the 4th line, as the 1st is the comment, second is the file link - * and the third is the code fence. - * The fourth line should have two spaces on the first line, as they should not be stripped. - */ - expect(data[3]).toMatch(/^\s\s/); + // Grab the first code block’s first line + const m = text.match(/```[^\n]*\n([\s\S]*?)\n```/); + expect(m).toBeTruthy(); + const firstCodeLine = m[1].split('\n')[0]; -}); + // With dedent OFF, snippet should still start with two spaces + expect(firstCodeLine).toMatch(/^\s{2}\S/); -test('Dedent snippets when option is set', async() => { + // Prose paragraph stays flush-left + expect(text).toMatch(/\nFor example, this paragraph starts at flush-left\./); - fs.copyFileSync(`${fixturesPath}/dedent.md`,`${testEnvPath}/dedent.md`); + // Nested list item keeps its two leading spaces + expect(text).toMatch(/\n - For example, this list item on another level/); +}); - cfg.origins = [ - { owner: 'temporalio', repo: 'samples-typescript' }, - ]; +test('Dedent when option is true (should only affect snippet; OTHER CONTENT UNCHANGED)', async () => { + fs.copyFileSync(`${fixturesPath}/dedent.md`, `${testEnvPath}/dedent.md`); + cfg.origins = [{ owner: 'temporalio', repo: 'samples-typescript' }]; cfg.features.enable_code_dedenting = true; const synctron = new Sync(cfg, logger); await synctron.run(); - let data = fs.readFileSync(`${testEnvPath}/dedent.md`, 'utf8'); - data = data.split("\n"); + const text = fs.readFileSync(`${testEnvPath}/dedent.md`, 'utf8'); + + const m = text.match(/```[^\n]*\n([\s\S]*?)\n```/); + expect(m).toBeTruthy(); + const firstCodeLine = m[1].split('\n')[0]; + + // EXPECTED (desired behavior): snippet becomes flush-left + expect(firstCodeLine).toMatch(/^\S/); - /* - * The code will start on the 4th line, as the 1st is the comment, second is the file link - * and the third is the code fence. - * The fourth line should NOT have two spaces at the start of the line, as they should be stripped. - */ - expect(data[3]).not.toMatch(/^\s/); + // EXPECTED (desired behavior): prose unchanged + expect(text).toMatch(/\nFor example, this paragraph starts at flush-left\./); + // EXPECTED (desired behavior): nested list item should remain indented + // This is what will FAIL with the current file-level dedent implementation, + // since it also strips indentation from list lines. + expect(text).toMatch(/\n - For example, this list item on another level/); }); test('Per snippet selectedLines configuration', async() => { From 5f736515cf9123f8d244fdfd4c7abd25c944f3c7 Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Thu, 11 Sep 2025 17:31:36 -0700 Subject: [PATCH 3/5] fix lint issue --- src/deindent.js | 8 ++++---- test/sync.test.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/deindent.js b/src/deindent.js index 591a478..2d6242c 100644 --- a/src/deindent.js +++ b/src/deindent.js @@ -1,6 +1,6 @@ function commonIndentPrefix(lines) { const nonEmpty = lines.filter(l => l.trim().length > 0); - if (nonEmpty.length === 0) return ""; + if (nonEmpty.length === 0) {return "";} // Optionally skip lines that are just a closing token const closingHead = /^(?:}|\]|\)|end\b)\s*$/; @@ -12,16 +12,16 @@ function commonIndentPrefix(lines) { let prefix = prefixes[0]; for (let i = 1; i < prefixes.length; i++) { let j = 0; - while (j < prefix.length && j < prefixes[i].length && prefix[j] === prefixes[i][j]) j++; + while (j < prefix.length && j < prefixes[i].length && prefix[j] === prefixes[i][j]) {j++;} prefix = prefix.slice(0, j); - if (prefix === "") break; + if (prefix === "") {break;} } return prefix; } function deindentByCommonPrefix(lines) { const prefix = commonIndentPrefix(lines); - if (!prefix) return lines.slice(); + if (!prefix) {return lines.slice();} const re = new RegExp("^" + prefix.replace(/[\t ]/g, m => (m === "\t" ? "\\t" : " "))); return lines.map(l => (l.startsWith(prefix) ? l.replace(re, "") : l)); } diff --git a/test/sync.test.js b/test/sync.test.js index df66d73..21058ad 100644 --- a/test/sync.test.js +++ b/test/sync.test.js @@ -181,7 +181,7 @@ test('No dedent when option is false (snippet stays indented; other content unch expect(text).toMatch(/\nFor example, this paragraph starts at flush-left\./); // Nested list item keeps its two leading spaces - expect(text).toMatch(/\n - For example, this list item on another level/); + expect(text).toMatch(/\n\s{2}- For example, this list item on another level/); }); test('Dedent when option is true (should only affect snippet; OTHER CONTENT UNCHANGED)', async () => { @@ -208,7 +208,7 @@ test('Dedent when option is true (should only affect snippet; OTHER CONTENT UNCH // EXPECTED (desired behavior): nested list item should remain indented // This is what will FAIL with the current file-level dedent implementation, // since it also strips indentation from list lines. - expect(text).toMatch(/\n - For example, this list item on another level/); + expect(text).toMatch(/\n\s{2}- For example, this list item on another level/); }); test('Per snippet selectedLines configuration', async() => { From badb3217a24380fb648374d1d35f5e121905b78e Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Fri, 12 Sep 2025 11:01:02 -0700 Subject: [PATCH 4/5] add more test coverage and account for edge cases --- src/deindent.js | 26 +++++++----- test/deindent.test.js | 94 +++++++++++++++++++++++++++++++++++++++++++ test/sync.test.js | 80 ++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 test/deindent.test.js diff --git a/src/deindent.js b/src/deindent.js index 2d6242c..ca31884 100644 --- a/src/deindent.js +++ b/src/deindent.js @@ -1,20 +1,24 @@ function commonIndentPrefix(lines) { const nonEmpty = lines.filter(l => l.trim().length > 0); - if (nonEmpty.length === 0) {return "";} + if (nonEmpty.length === 0) return ""; - // Optionally skip lines that are just a closing token - const closingHead = /^(?:}|\]|\)|end\b)\s*$/; - const candidates = nonEmpty.filter(l => !closingHead.test(l.trim())); - const pool = candidates.length ? candidates : nonEmpty; + // Treat lines that are only closing tokens (possibly multiple) as "closers". + // Examples matched: "}", ")", "]", "});", "],", "})", "));", etc., with optional spaces. + const CLOSING_ONLY = /^\s*[\]\)}]+(?:[;,])?\s*$/; + const RUBY_END = /^\s*end\b\s*$/; + const isClosingOnly = (s) => CLOSING_ONLY.test(s) || RUBY_END.test(s); - // Compute common prefix of leading whitespace (tabs/spaces preserved) - const prefixes = pool.map(l => (l.match(/^[\t ]*/)[0] || "")); - let prefix = prefixes[0]; + // Ignore closers when computing the common indent + const pool = nonEmpty.filter(l => !isClosingOnly(l.trim())); + const candidates = pool.length ? pool : nonEmpty; + + const prefixes = candidates.map(l => (l.match(/^[\t ]*/)?.[0] || "")); + let prefix = prefixes[0] || ""; for (let i = 1; i < prefixes.length; i++) { let j = 0; - while (j < prefix.length && j < prefixes[i].length && prefix[j] === prefixes[i][j]) {j++;} + while (j < prefix.length && j < prefixes[i].length && prefix[j] === prefixes[i][j]) j++; prefix = prefix.slice(0, j); - if (prefix === "") {break;} + if (!prefix) break; } return prefix; } @@ -28,4 +32,4 @@ function deindentByCommonPrefix(lines) { const SENSITIVE_INDENT_EXTS = new Set(['make', 'mk', 'Makefile', 'diff']); -module.exports = { deindentByCommonPrefix, SENSITIVE_INDENT_EXTS }; +module.exports = { commonIndentPrefix, deindentByCommonPrefix, SENSITIVE_INDENT_EXTS }; diff --git a/test/deindent.test.js b/test/deindent.test.js new file mode 100644 index 0000000..f3ef54b --- /dev/null +++ b/test/deindent.test.js @@ -0,0 +1,94 @@ +const { deindentByCommonPrefix, commonIndentPrefix } = require('../src/deindent'); + +describe('deindentByCommonPrefix', () => { + test('strips common leading spaces from all lines', () => { + const input = [ + ' foo();', + ' bar();', + ]; + const output = deindentByCommonPrefix(input); + expect(output).toEqual(['foo();', 'bar();']); + }); + + test('preserves relative indentation inside the snippet', () => { + const input = [ + ' if (x) {', + ' doSomething();', + ' }', + ]; + const output = deindentByCommonPrefix(input); + expect(output).toEqual([ + 'if (x) {', + ' doSomething();', + '}', + ]); + }); + + test('does not dedent when common indent is zero (hanging indent case)', () => { + const input = [ + ' a++;', + 'b = a;', + ]; + const output = deindentByCommonPrefix(input); + // Nothing stripped + expect(output).toEqual(input); + }); + + test('ignores closing-only head lines when computing indent', () => { + const input = [ + '});', + ' doSomething();', + ]; + const output = deindentByCommonPrefix(input); + expect(output).toEqual([ + '});', // unchanged + 'doSomething();' // dedented + ]); + }); + + test('handles empty lines gracefully', () => { + const input = [ + '', + ' foo();', + '', + ' bar();', + ]; + const output = deindentByCommonPrefix(input); + expect(output).toEqual([ + '', + 'foo();', + '', + 'bar();', + ]); + }); + + test('returns a shallow copy when there is nothing to dedent', () => { + const input = ['foo();', 'bar();']; + const output = deindentByCommonPrefix(input); + expect(output).toEqual(input); + expect(output).not.toBe(input); // new array, not the same reference + }); +}); + +describe('commonIndentPrefix', () => { + test('computes the correct common indent', () => { + const input = [ + ' foo();', + ' bar();', + ]; + expect(commonIndentPrefix(input)).toBe(' '); + }); + + test('returns empty string when lines have different starting indents and at least one substantive line is flush left', () => { + const input = [ + ' foo();', + 'bar();', + ]; + expect(commonIndentPrefix(input)).toBe(''); + }); + + test('returns empty string for all-empty input', () => { + expect(commonIndentPrefix([])).toBe(''); + expect(commonIndentPrefix(['', ''])).toBe(''); + }); +}); diff --git a/test/sync.test.js b/test/sync.test.js index 21058ad..64e7c8d 100644 --- a/test/sync.test.js +++ b/test/sync.test.js @@ -158,6 +158,62 @@ test('uses regex patterns to pare down snippet inserted into a file', async() => }); +test('Dedent keeps relative indentation inside the snippet', async () => { + fs.copyFileSync(`${fixturesPath}/dedent.md`, `${testEnvPath}/dedent.md`); + + cfg.origins = [{ owner: 'temporalio', repo: 'samples-typescript' }]; + cfg.features.enable_code_dedenting = true; + + const synctron = new Sync(cfg, logger); + await synctron.run(); + + const text = fs.readFileSync(`${testEnvPath}/dedent.md`, 'utf8'); + + // Grab the first fenced code block contents + const m = text.match(/```[^\n]*\n([\s\S]*?)\n```/); + expect(m).toBeTruthy(); + + const bodyLines = m[1].split('\n').filter(l => l.length > 0); + const indents = bodyLines.map(l => (l.match(/^[ \t]*/)?.[0].length ?? 0)); + const minIndent = Math.min(...indents); + + // With dedent enabled, the common left padding is removed: + expect(minIndent).toBe(0); + + // But relative indentation remains for inner lines (at least one line still indented): + expect(bodyLines.some(l => /^[ \t]+\S/.test(l))).toBe(true); +}); + +test('Regex-selected regions are dedented after start/end pattern slicing', async () => { + fs.copyFileSync(`${fixturesPath}/regex_index.md`, `${testEnvPath}/regex_index.md`); + + cfg.origins = [ + { owner: 'temporalio', repo: 'money-transfer-project-template-go' }, + { owner: 'temporalio', repo: 'samples-typescript' }, + ]; + cfg.features.enable_code_dedenting = true; + + const synctron = new Sync(cfg, logger); + await synctron.run(); + + const text = fs.readFileSync(`${testEnvPath}/regex_index.md`, 'utf8'); + + // First fenced block (per your fixture) + const m = text.match(/```[^\n]*\n([\s\S]*?)\n```/); + expect(m).toBeTruthy(); + const bodyLines = m[1].split('\n').filter(l => l.length > 0); + const indents = bodyLines.map(l => (l.match(/^[ \t]*/)?.[0].length ?? 0)); + const minIndent = Math.min(...indents); + + // After slicing by start/end patterns, we still dedent the selected region: + expect(minIndent).toBe(0); + + // Keep original regex expectations for content sanity + expect(text).not.toMatch(/import type \* as activities/); + expect(text).toMatch(/const \{ greet/); + expect(text).not.toMatch(/export async function example/); +}); + test('No dedent when option is false (snippet stays indented; other content unchanged)', async () => { fs.copyFileSync(`${fixturesPath}/dedent.md`, `${testEnvPath}/dedent.md`); @@ -211,6 +267,30 @@ test('Dedent when option is true (should only affect snippet; OTHER CONTENT UNCH expect(text).toMatch(/\n\s{2}- For example, this list item on another level/); }); +test('Dedent works without fences (enable_code_block=false)', async () => { + fs.copyFileSync(`${fixturesPath}/dedent.md`, `${testEnvPath}/dedent.md`); + + cfg.origins = [{ owner: 'temporalio', repo: 'samples-typescript' }]; + cfg.features.enable_code_block = false; // no ``` + cfg.features.enable_code_dedenting = true; // dedent ON + + const synctron = new Sync(cfg, logger); + await synctron.run(); + + const text = fs.readFileSync(`${testEnvPath}/dedent.md`, 'utf8'); + + // Extract snippet region between markers (simple, inline) + const m = text.match(/\n([\s\S]*?)\n/); + expect(m).toBeTruthy(); + const bodyLines = m[1].split('\n').filter(l => l.length > 0); + + // Same invariants: min indent is 0; at least one line still indented + const indents = bodyLines.map(l => (l.match(/^[ \t]*/)?.[0].length ?? 0)); + expect(Math.min(...indents)).toBe(0); + expect(bodyLines.some(l => /^[ \t]+\S/.test(l))).toBe(true); +}); + + test('Per snippet selectedLines configuration', async() => { cfg.origins = [ From 25d975f39b31f24a5aa364e145b7f4365c29d935 Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Fri, 12 Sep 2025 11:10:08 -0700 Subject: [PATCH 5/5] fix linting --- src/deindent.js | 12 +++++++----- test/deindent.test.js | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/deindent.js b/src/deindent.js index ca31884..418fd3d 100644 --- a/src/deindent.js +++ b/src/deindent.js @@ -1,10 +1,10 @@ function commonIndentPrefix(lines) { const nonEmpty = lines.filter(l => l.trim().length > 0); - if (nonEmpty.length === 0) return ""; + if (nonEmpty.length === 0) {return "";} // Treat lines that are only closing tokens (possibly multiple) as "closers". // Examples matched: "}", ")", "]", "});", "],", "})", "));", etc., with optional spaces. - const CLOSING_ONLY = /^\s*[\]\)}]+(?:[;,])?\s*$/; + const CLOSING_ONLY = /^\s*[\])}]+(?:[;,])?\s*$/; const RUBY_END = /^\s*end\b\s*$/; const isClosingOnly = (s) => CLOSING_ONLY.test(s) || RUBY_END.test(s); @@ -16,16 +16,18 @@ function commonIndentPrefix(lines) { let prefix = prefixes[0] || ""; for (let i = 1; i < prefixes.length; i++) { let j = 0; - while (j < prefix.length && j < prefixes[i].length && prefix[j] === prefixes[i][j]) j++; + while (j < prefix.length && j < prefixes[i].length && prefix[j] === prefixes[i][j]) {j++;} prefix = prefix.slice(0, j); - if (!prefix) break; + if (!prefix) {break;} } return prefix; } function deindentByCommonPrefix(lines) { const prefix = commonIndentPrefix(lines); - if (!prefix) {return lines.slice();} + if (!prefix) { + return lines.slice(); + } const re = new RegExp("^" + prefix.replace(/[\t ]/g, m => (m === "\t" ? "\\t" : " "))); return lines.map(l => (l.startsWith(prefix) ? l.replace(re, "") : l)); } diff --git a/test/deindent.test.js b/test/deindent.test.js index f3ef54b..891d32a 100644 --- a/test/deindent.test.js +++ b/test/deindent.test.js @@ -42,7 +42,7 @@ describe('deindentByCommonPrefix', () => { const output = deindentByCommonPrefix(input); expect(output).toEqual([ '});', // unchanged - 'doSomething();' // dedented + 'doSomething();', // dedented ]); });