Repository: mdx-js/mdx-analyzer
Pull request #370 (2023-12-14 16:19:17)

Dedent virtual markdown chunks
### Initial checklist

*   [x] I read the support docs <!-- https://github.com/unifiedjs/.github/blob/main/support.md -->
*   [x] I read the contributing guide <!-- https://github.com/unifiedjs/.github/blob/main/contributing.md -->
*   [x] I agree to follow the code of conduct <!-- https://github.com/unifiedjs/.github/blob/main/code-of-conduct.md -->
*   [x] I searched issues and couldn’t find anything (or linked relevant results below) <!-- https://github.com/search?q=user%3Aunifiedjs&type=Issues -->
*   [x] If applicable, I’ve added docs and tests

### Description of changes

MDX allows to use indentation. In regular markdown however, indented code represents a code block. To work around this, all virtual markdown chunks are now dedented. This may change the semantic meaning of certain markdown, but it doesn’t affect editor features as far as I’m aware.

<!--do not edit: pr-->


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 0c08e7a2730e731658805040427e80101a7fd977

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/mdx-js/mdx-analyzer/new/dedent-markdown?filename=.changeset/rude-tigers-type.md&value=---%0A%22%40mdx-js%2Flanguage-service%22%3A%20patch%0A---%0A%0ADedent%20virtual%20markdown%20chunks%0A)


github-actions[bot]: Hi! This is accepted and can go somewhere!

Team: please review this PR and use the `area/*` (to describe the scope of the change), `platform/*` (if this is related to a specific one), and `semver/*` and `type/*` labels to annotate this.

<!--xxx:beep-boop:yes-maintainers-->

Patch:
diff --git a/packages/language-service/lib/language-module.js b/packages/language-service/lib/language-module.js
index 4861a59..ff9ae5c 100644
--- a/packages/language-service/lib/language-module.js
+++ b/packages/language-service/lib/language-module.js
@@ -96,9 +96,24 @@ function addOffset(mapping, source, generated, startOffset, endOffset) {
     return generated
   }
 
-  mapping.sourceOffsets.push(startOffset)
-  mapping.generatedOffsets.push(generated.length)
-  mapping.lengths.push(endOffset - startOffset)
+  const length = endOffset - startOffset
+  const previousSourceOffset = mapping.sourceOffsets.at(-1)
+  const previousGeneratedOffset = mapping.generatedOffsets.at(-1)
+  const previousLength = mapping.lengths.at(-1)
+  if (
+    previousSourceOffset !== undefined &&
+    previousGeneratedOffset !== undefined &&
+    previousLength !== undefined &&
+    previousSourceOffset + previousLength === startOffset &&
+    previousGeneratedOffset + previousLength === generated.length
+  ) {
+    mapping.lengths[mapping.lengths.length - 1] += length
+  } else {
+    mapping.sourceOffsets.push(startOffset)
+    mapping.generatedOffsets.push(generated.length)
+    mapping.lengths.push(length)
+  }
+
   return generated + source.slice(startOffset, endOffset)
 }
 
@@ -323,13 +338,30 @@ function getVirtualFiles(fileName, snapshot, ts, processor) {
    */
   function updateMarkdownFromOffsets(startOffset, endOffset) {
     if (nextMarkdownSourceStart !== startOffset) {
-      markdown = addOffset(
-        markdownMapping,
-        mdx,
-        markdown,
-        nextMarkdownSourceStart,
-        startOffset
-      )
+      const slice = mdx.slice(nextMarkdownSourceStart, startOffset)
+      for (const match of slice.matchAll(/^[\t ]*(.*\r?\n?)/gm)) {
+        if (match.index === undefined) {
+          continue
+        }
+
+        const [line, lineContent] = match
+        if (line.length === 0) {
+          continue
+        }
+
+        const lineEnd = nextMarkdownSourceStart + match.index + line.length
+        let lineStart = lineEnd - lineContent.length
+        if (
+          match.index === 0 &&
+          nextMarkdownSourceStart !== 0 &&
+          mdx[lineStart - 1] !== '\n'
+        ) {
+          lineStart = nextMarkdownSourceStart + match.index
+        }
+
+        markdown = addOffset(markdownMapping, mdx, markdown, lineStart, lineEnd)
+      }
+
       if (startOffset !== endOffset) {
         markdown += '<!---->'
       }
diff --git a/release.sh b/release.sh
index 340879c..396d496 100755
--- a/release.sh
+++ b/release.sh
@@ -3,6 +3,7 @@ npm pack --pack-destination "$PWD" --workspace @mdx-js/language-service
 npm pack --pack-destination "$PWD" --workspace @mdx-js/language-server
 npx --workspace vscode-mdx vsce package --out "$PWD"
 
+node scripts/tag-extension.mjs
 npx changeset publish
 npx ovsx publish --packagePath ./*.vsix
 npx vsce publish --packagePath ./*.vsix
diff --git a/scripts/tag-extension.mjs b/scripts/tag-extension.mjs
new file mode 100755
index 0000000..ea76f8d
--- /dev/null
+++ b/scripts/tag-extension.mjs
@@ -0,0 +1,24 @@
+#!/usr/bin/env node
+import assert from 'node:assert/strict'
+import {spawnSync} from 'node:child_process'
+// eslint-disable-next-line import/order
+import {createRequire} from 'node:module'
+
+const require = createRequire(import.meta.url)
+const pkg = require('../packages/vscode-mdx/package.json')
+
+const tag = `${pkg.name}@${pkg.version}`
+
+const {status, stdout, error} = spawnSync('git', [
+  'ls-remote',
+  pkg.repository.url,
+  tag
+])
+
+assert.equal(status, 0, error)
+
+const exists = String(stdout).trim() !== ''
+
+if (!exists) {
+  console.log(`\nNew tag: ${tag}`)
+}


Test patch:
diff --git a/packages/language-service/test/language-module.js b/packages/language-service/test/language-module.js
index ce96104..b74f805 100644
--- a/packages/language-service/test/language-module.js
+++ b/packages/language-service/test/language-module.js
@@ -1582,6 +1582,95 @@ test('create virtual file w/ mdxTextExpression', () => {
   })
 })
 
+test('create virtual file w/ dedented markdown content', () => {
+  const module = getLanguageModule(typescript)
+
+  const snapshot = snapshotFromLines(
+    '     | Language |',
+    ' | --- |',
+    '            | MDX |',
+    '     | JavaScript |',
+    '| TypeScript |'
+  )
+
+  const file = module.createVirtualFile('/test.mdx', 'mdx', snapshot)
+
+  assert.deepEqual(file, {
+    fileName: '/test.mdx',
+    languageId: 'mdx',
+    mappings: [
+      {
+        sourceOffsets: [0],
+        generatedOffsets: [0],
+        lengths: [81],
+        data: {
+          completion: true,
+          format: true,
+          navigation: true,
+          semantic: true,
+          structure: true,
+          verification: true
+        }
+      }
+    ],
+    snapshot,
+    embeddedFiles: [
+      {
+        embeddedFiles: [],
+        fileName: '/test.mdx.jsx',
+        languageId: 'javascriptreact',
+        typescript: {
+          scriptKind: 2
+        },
+        mappings: [],
+        snapshot: snapshotFromLines(
+          '',
+          '/**',
+          ' * Render the MDX contents.',
+          ' *',
+          ' * @param {{readonly [K in keyof MDXContentProps]: MDXContentProps[K]}} props',
+          ' *   The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component.',
+          ' */',
+          'export default function MDXContent(props) {',
+          "  return <><>{''}</></>",
+          '}',
+          '',
+          '// @ts-ignore',
+          '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */',
+          ''
+        )
+      },
+      {
+        embeddedFiles: [],
+        fileName: '/test.mdx.md',
+        languageId: 'markdown',
+        mappings: [
+          {
+            sourceOffsets: [5, 19, 39, 52],
+            generatedOffsets: [0, 13, 21, 29],
+            lengths: [13, 8, 8, 29],
+            data: {
+              completion: true,
+              format: false,
+              navigation: true,
+              semantic: true,
+              structure: true,
+              verification: true
+            }
+          }
+        ],
+        snapshot: snapshotFromLines(
+          '| Language |',
+          '| --- |',
+          '| MDX |',
+          '| JavaScript |',
+          '| TypeScript |'
+        )
+      }
+    ]
+  })
+})
+
 test('create virtual file w/ syntax error', () => {
   const module = getLanguageModule(typescript)