diff --git a/CHANGELOG.md b/CHANGELOG.md index aa35844..c8f482f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.5.0-alpha.10 — Unreleased - Improve handling of encoded path completions. +- Improve reliability of smart selection for links and tweak behavior. ## 0.5.0-alpha.9 — March 24, 2025 - Improved detection of multi-line links. diff --git a/src/index.ts b/src/index.ts index f44090d..24c2143 100644 --- a/src/index.ts +++ b/src/index.ts @@ -256,11 +256,11 @@ export function createLanguageService(init: LanguageServiceInitialization): IMdL const logger = init.logger; const tocProvider = new MdTableOfContentsProvider(init.parser, init.workspace, logger); - const smartSelectProvider = new MdSelectionRangeProvider(init.parser, tocProvider, logger); const foldingProvider = new MdFoldingProvider(init.parser, tocProvider, logger); const linkProvider = new MdLinkProvider(config, init.parser, init.workspace, tocProvider, logger); const pathCompletionProvider = new MdPathCompletionProvider(config, init.workspace, init.parser, linkProvider, tocProvider); const linkCache = createWorkspaceLinkCache(init.parser, init.workspace); + const smartSelectProvider = new MdSelectionRangeProvider(init.parser, tocProvider, linkProvider, logger); const referencesProvider = new MdReferencesProvider(config, init.parser, init.workspace, tocProvider, linkCache, logger); const definitionsProvider = new MdDefinitionProvider(config, init.workspace, tocProvider, linkCache); const renameProvider = new MdRenameProvider(config, init.workspace, init.parser, referencesProvider, tocProvider, init.parser.slugifier, logger); diff --git a/src/languageFeatures/diagnostics.ts b/src/languageFeatures/diagnostics.ts index be425cd..d2e27d9 100644 --- a/src/languageFeatures/diagnostics.ts +++ b/src/languageFeatures/diagnostics.ts @@ -379,7 +379,7 @@ export class DiagnosticComputer { if (!resolvedHrefPath) { for (const link of links) { - if (!this.#isIgnoredLink(options, link.source.pathText)) { + if (!this.#isIgnoredLink(options, link.source.hrefPathText)) { diagnostics.push({ code: DiagnosticCode.link_noSuchFile, message: l10n.t('File does not exist at path: {0}', path.fsPath), @@ -387,7 +387,7 @@ export class DiagnosticComputer { severity: pathErrorSeverity, data: { fsPath: path.fsPath, - hrefText: link.source.pathText, + hrefText: link.source.hrefPathText, } }); } @@ -407,8 +407,8 @@ export class DiagnosticComputer { continue; } - if (!(toc && tocLookupByLink(toc, link)) && !this.#isIgnoredLink(options, link.source.pathText) && !this.#isIgnoredLink(options, link.source.hrefText)) { - const range = (link.source.fragmentRange && modifyRange(link.source.fragmentRange, translatePosition(link.source.fragmentRange.start, { characterDelta: -1 }), undefined)) ?? link.source.hrefRange; + if (!(toc && tocLookupByLink(toc, link)) && !this.#isIgnoredLink(options, link.source.hrefPathText) && !this.#isIgnoredLink(options, link.source.hrefText)) { + const range = (link.source.hrefFragmentRange && modifyRange(link.source.hrefFragmentRange, translatePosition(link.source.hrefFragmentRange.start, { characterDelta: -1 }), undefined)) ?? link.source.hrefRange; diagnostics.push({ code: DiagnosticCode.link_noSuchHeaderInFile, message: l10n.t('Header does not exist in file: {0}', link.fragment), diff --git a/src/languageFeatures/documentHighlights.ts b/src/languageFeatures/documentHighlights.ts index 5b255d2..5a115fc 100644 --- a/src/languageFeatures/documentHighlights.ts +++ b/src/languageFeatures/documentHighlights.ts @@ -58,11 +58,11 @@ export class MdDocumentHighlightProvider { for (const link of links) { if (link.href.kind === HrefKind.Internal && toc.lookupByFragment(link.href.fragment) === header - && link.source.fragmentRange + && link.source.hrefFragmentRange && isSameResource(link.href.path, docUri) ) { yield { - range: modifyRange(link.source.fragmentRange, translatePosition(link.source.fragmentRange.start, { characterDelta: -1 })), + range: modifyRange(link.source.hrefFragmentRange, translatePosition(link.source.hrefFragmentRange.start, { characterDelta: -1 })), kind: lsp.DocumentHighlightKind.Read, }; } @@ -85,7 +85,7 @@ export class MdDocumentHighlightProvider { return this.#getHighlightsForReference(link.href.ref, links); } case HrefKind.Internal: { - if (link.source.fragmentRange && rangeContains(link.source.fragmentRange, position)) { + if (link.source.hrefFragmentRange && rangeContains(link.source.hrefFragmentRange, position)) { return this.#getHighlightsForLinkFragment(document, link.href, links, toc); } @@ -114,9 +114,9 @@ export class MdDocumentHighlightProvider { for (const link of links) { if (link.href.kind === HrefKind.Internal && looksLikePathToResource(this.#configuration, link.href.path, targetDoc)) { - if (link.source.fragmentRange && link.href.fragment.toLowerCase() === fragment) { + if (link.source.hrefFragmentRange && link.href.fragment.toLowerCase() === fragment) { yield { - range: modifyRange(link.source.fragmentRange, translatePosition(link.source.fragmentRange.start, { characterDelta: -1 })), + range: modifyRange(link.source.hrefFragmentRange, translatePosition(link.source.hrefFragmentRange.start, { characterDelta: -1 })), kind: lsp.DocumentHighlightKind.Read, }; } diff --git a/src/languageFeatures/documentLinks.ts b/src/languageFeatures/documentLinks.ts index 94e081e..7e22036 100644 --- a/src/languageFeatures/documentLinks.ts +++ b/src/languageFeatures/documentLinks.ts @@ -111,11 +111,11 @@ function getFragmentRange(text: string, start: lsp.Position, end: lsp.Position): return { start: translatePosition(start, { characterDelta: index + 1 }), end }; } -function getLinkSourceFragmentInfo(document: ITextDocument, link: string, linkStart: lsp.Position, linkEnd: lsp.Position): { fragmentRange: lsp.Range | undefined; pathText: string } { +function getLinkSourceFragmentInfo(document: ITextDocument, link: string, linkStart: lsp.Position, linkEnd: lsp.Position): { hrefFragmentRange: lsp.Range | undefined; hrefPathText: string } { const fragmentRange = getFragmentRange(link, linkStart, linkEnd); return { - pathText: document.getText({ start: linkStart, end: fragmentRange ? translatePosition(fragmentRange.start, { characterDelta: -1 }) : linkEnd }), - fragmentRange, + hrefPathText: document.getText({ start: linkStart, end: fragmentRange ? translatePosition(fragmentRange.start, { characterDelta: -1 }) : linkEnd }), + hrefFragmentRange: fragmentRange, }; } @@ -448,18 +448,20 @@ export class MdLinkComputer { } const linkEnd = translatePosition(linkStart, { characterDelta: match[0].length }); - const hrefRange = { start: hrefStart, end: hrefEnd }; yield { kind: MdLinkKind.Link, source: { isAngleBracketLink: false, hrefText: reference, - pathText: reference, + hrefPathText: reference, resource: getDocUri(document), range: { start: linkStart, end: linkEnd }, - targetRange: hrefRange, - hrefRange: hrefRange, - fragmentRange: undefined, + targetRange: lsp.Range.create( + translatePosition(hrefStart, { characterDelta: -1 }), + translatePosition(hrefEnd, { characterDelta: 1 }) + ), + hrefRange: lsp.Range.create(hrefStart, hrefEnd), + hrefFragmentRange: undefined, }, href: { kind: HrefKind.Reference, diff --git a/src/languageFeatures/fileRename.ts b/src/languageFeatures/fileRename.ts index 5dc4b3e..d182c7a 100644 --- a/src/languageFeatures/fileRename.ts +++ b/src/languageFeatures/fileRename.ts @@ -116,7 +116,7 @@ export class MdFileRenameProvider { } // If the link was within a file in the moved dir but traversed out of it, we also need to update the path - if (link.source.pathText.startsWith('..') && isParentDir(edit.newUri, docUri)) { + if (link.source.hrefPathText.startsWith('..') && isParentDir(edit.newUri, docUri)) { // Resolve the link relative to the old file path const oldDocUri = docUri.with({ path: Utils.joinPath(edit.oldUri, path.posix.relative(edit.newUri.path, docUri.path)).path @@ -136,7 +136,7 @@ export class MdFileRenameProvider { } const replacementPath = encodeURI(newPathText); - if (replacementPath !== link.source.pathText) { + if (replacementPath !== link.source.hrefPathText) { const { range, newText } = getLinkRenameEdit(link, replacementPath); builder.replace(docUri, range, newText); didParticipate = true; @@ -240,7 +240,7 @@ export class MdFileRenameProvider { } const newFilePath = removeNewUriExtIfNeeded(this.#config, link.href, newUri); - const newLinkText = getLinkRenameText(this.#workspace, link.source, newFilePath, link.source.pathText.startsWith('.')); + const newLinkText = getLinkRenameText(this.#workspace, link.source, newFilePath, link.source.hrefPathText.startsWith('.')); if (typeof newLinkText === 'string') { const { range, newText } = getLinkRenameEdit(link, newLinkText); builder.replace(doc, range, newText); diff --git a/src/languageFeatures/references.ts b/src/languageFeatures/references.ts index f248426..3a27727 100644 --- a/src/languageFeatures/references.ts +++ b/src/languageFeatures/references.ts @@ -226,7 +226,7 @@ export class MdReferencesProvider extends Disposable { const references: MdReference[] = []; - if (resolvedResource && this.#isMarkdownPath(resolvedResource) && sourceLink.href.fragment && sourceLink.source.fragmentRange && rangeContains(sourceLink.source.fragmentRange, triggerPosition)) { + if (resolvedResource && this.#isMarkdownPath(resolvedResource) && sourceLink.href.fragment && sourceLink.source.hrefFragmentRange && rangeContains(sourceLink.source.hrefFragmentRange, triggerPosition)) { const toc = await this.#tocProvider.get(resolvedResource); const entry = toc?.lookupByFragment(sourceLink.href.fragment); if (entry) { @@ -325,8 +325,8 @@ export class MdReferencesProvider extends Disposable { * Get just the range of the file path, dropping the fragment */ #getPathRange(link: MdLink): lsp.Range { - return link.source.fragmentRange - ? modifyRange(link.source.hrefRange, undefined, translatePosition(link.source.fragmentRange.start, { characterDelta: -1 })) + return link.source.hrefFragmentRange + ? modifyRange(link.source.hrefRange, undefined, translatePosition(link.source.hrefFragmentRange.start, { characterDelta: -1 })) : link.source.hrefRange; } } diff --git a/src/languageFeatures/rename.ts b/src/languageFeatures/rename.ts index 23a51ec..e7c3e48 100644 --- a/src/languageFeatures/rename.ts +++ b/src/languageFeatures/rename.ts @@ -103,12 +103,12 @@ export class MdRenameProvider { } // See if we are renaming the fragment or the path - const { fragmentRange } = triggerRef.link.source; - if (fragmentRange && rangeContains(fragmentRange, position)) { + const { hrefFragmentRange } = triggerRef.link.source; + if (hrefFragmentRange && rangeContains(hrefFragmentRange, position)) { const declaration = this.#findHeaderDeclaration(allRefsInfo.references); return { - range: fragmentRange, - placeholder: declaration ? declaration.headerText : document.getText(fragmentRange), + range: hrefFragmentRange, + placeholder: declaration ? declaration.headerText : document.getText(hrefFragmentRange), }; } @@ -141,9 +141,9 @@ export class MdRenameProvider { return this.#renameReferenceLinks(allRefsInfo, newName); } else if (triggerRef.kind === MdReferenceKind.Link && triggerRef.link.href.kind === HrefKind.External) { return this.#renameExternalLink(allRefsInfo, newName); - } else if (triggerRef.kind === MdReferenceKind.Header || (triggerRef.kind === MdReferenceKind.Link && triggerRef.link.source.fragmentRange && rangeContains(triggerRef.link.source.fragmentRange, position) && (triggerRef.link.kind === MdLinkKind.Definition || triggerRef.link.kind === MdLinkKind.Link && triggerRef.link.href.kind === HrefKind.Internal))) { + } else if (triggerRef.kind === MdReferenceKind.Header || (triggerRef.kind === MdReferenceKind.Link && triggerRef.link.source.hrefFragmentRange && rangeContains(triggerRef.link.source.hrefFragmentRange, position) && (triggerRef.link.kind === MdLinkKind.Definition || triggerRef.link.kind === MdLinkKind.Link && triggerRef.link.href.kind === HrefKind.Internal))) { return this.#renameFragment(allRefsInfo, newName, token); - } else if (triggerRef.kind === MdReferenceKind.Link && !(triggerRef.link.source.fragmentRange && rangeContains(triggerRef.link.source.fragmentRange, position)) && (triggerRef.link.kind === MdLinkKind.Link || triggerRef.link.kind === MdLinkKind.Definition) && triggerRef.link.href.kind === HrefKind.Internal) { + } else if (triggerRef.kind === MdReferenceKind.Link && !(triggerRef.link.source.hrefFragmentRange && rangeContains(triggerRef.link.source.hrefFragmentRange, position)) && (triggerRef.link.kind === MdLinkKind.Link || triggerRef.link.kind === MdLinkKind.Definition) && triggerRef.link.href.kind === HrefKind.Internal) { return this.#renameFilePath(triggerRef.link.source.resource, triggerRef.link.href, allRefsInfo, newName, token); } @@ -263,7 +263,7 @@ export class MdRenameProvider { for (const ref of refs?.references ?? []) { if (ref.kind === MdReferenceKind.Link) { - builder.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, changedHeader.slug.value); + builder.replace(ref.link.source.resource, ref.link.source.hrefFragmentRange ?? ref.location.range, changedHeader.slug.value); } } } @@ -277,7 +277,7 @@ export class MdRenameProvider { break; case MdReferenceKind.Link: - builder.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, !ref.link.source.fragmentRange || ref.link.href.kind === HrefKind.External ? newHeaderText : newSlug.value); + builder.replace(ref.link.source.resource, ref.link.source.hrefFragmentRange ?? ref.location.range, !ref.link.source.hrefFragmentRange || ref.link.href.kind === HrefKind.External ? newHeaderText : newSlug.value); break; } } @@ -302,7 +302,7 @@ export class MdRenameProvider { if (ref.link.kind === MdLinkKind.Definition) { builder.replace(ref.link.source.resource, ref.link.ref.range, newName); } else { - builder.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, newName); + builder.replace(ref.link.source.resource, ref.link.source.hrefFragmentRange ?? ref.location.range, newName); } } } @@ -356,8 +356,8 @@ export function getLinkRenameText(workspace: IWorkspace, source: MdLinkSource, n } export function getFilePathRange(link: MdLink): lsp.Range { - if (link.source.fragmentRange) { - return modifyRange(link.source.hrefRange, undefined, translatePosition(link.source.fragmentRange.start, { characterDelta: -1 })); + if (link.source.hrefFragmentRange) { + return modifyRange(link.source.hrefRange, undefined, translatePosition(link.source.hrefFragmentRange.start, { characterDelta: -1 })); } return link.source.hrefRange; } diff --git a/src/languageFeatures/smartSelect.ts b/src/languageFeatures/smartSelect.ts index 3ebe3c3..0e1a1d1 100644 --- a/src/languageFeatures/smartSelect.ts +++ b/src/languageFeatures/smartSelect.ts @@ -11,20 +11,26 @@ import { areRangesEqual, modifyRange, rangeContains } from '../types/range'; import { getLine, ITextDocument } from '../types/textDocument'; import { coalesce } from '../util/arrays'; import { isEmptyOrWhitespace } from '../util/string'; +import { MdLinkProvider } from './documentLinks'; +import { HrefKind } from '../types/documentLink'; export class MdSelectionRangeProvider { readonly #parser: IMdParser; readonly #tocProvider: MdTableOfContentsProvider; + readonly #linkProvider: MdLinkProvider; + readonly #logger: ILogger; constructor( parser: IMdParser, tocProvider: MdTableOfContentsProvider, + documentLink: MdLinkProvider, logger: ILogger, ) { this.#parser = parser; this.#tocProvider = tocProvider; + this.#linkProvider = documentLink; this.#logger = logger; } @@ -45,7 +51,11 @@ export class MdSelectionRangeProvider { return; } - const inlineRange = createInlineRange(document, position, blockRange); + const inlineRange = await createInlineRange(document, position, blockRange, this.#linkProvider, token); + if (token.isCancellationRequested) { + return; + } + return inlineRange ?? blockRange ?? headerRange; } @@ -145,7 +155,7 @@ function createBlockRange(block: TokenWithMap, document: ITextDocument, cursorLi } } -function createInlineRange(document: ITextDocument, cursorPosition: lsp.Position, parent?: lsp.SelectionRange): lsp.SelectionRange | undefined { +async function createInlineRange(document: ITextDocument, cursorPosition: lsp.Position, parent: lsp.SelectionRange | undefined, linkProvider: MdLinkProvider, token: lsp.CancellationToken): Promise { const lineText = getLine(document, cursorPosition.line); const boldSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, parent); const italicSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, parent); @@ -157,7 +167,12 @@ function createInlineRange(document: ITextDocument, cursorPosition: lsp.Position comboSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, italicSelection); } } - const linkSelection = createLinkRange(lineText, cursorPosition.character, cursorPosition.line, comboSelection ?? boldSelection ?? italicSelection ?? parent); + + const linkSelection = await createLinkRange(document, cursorPosition, comboSelection ?? boldSelection ?? italicSelection ?? parent, linkProvider, token); + if (token.isCancellationRequested) { + return; + } + const inlineCodeBlockSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, false, linkSelection ?? parent); return inlineCodeBlockSelection ?? linkSelection ?? comboSelection ?? boldSelection ?? italicSelection; } @@ -218,30 +233,42 @@ function createOtherInlineRange(lineText: string, cursorChar: number, cursorLine return undefined; } -function createLinkRange(lineText: string, cursorChar: number, cursorLine: number, parent?: lsp.SelectionRange): lsp.SelectionRange | undefined { - const regex = /(\[[^\(\)]*\])(\([^\[\]]*\))/g; - const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length > cursorChar); - - if (matches.length) { - // should only be one match, so select first and index 0 contains the entire match, so match = [text](url) - const link = matches[0][0]; - const linkRange = makeSelectionRange(lsp.Range.create(cursorLine, lineText.indexOf(link), cursorLine, lineText.indexOf(link) + link.length), parent); - - const linkText = matches[0][1]; - const url = matches[0][2]; +async function createLinkRange(document: ITextDocument, cursorPos: lsp.Position, parent: lsp.SelectionRange | undefined, linkProvider: MdLinkProvider, token: lsp.CancellationToken): Promise { + const links = await linkProvider.getLinks(document); + if (token.isCancellationRequested) { + return; + } - // determine if cursor is within [text] or (url) in order to know which should be selected - const nearestType = cursorChar >= lineText.indexOf(linkText) && cursorChar < lineText.indexOf(linkText) + linkText.length ? linkText : url; + const link = links.links.find(link => rangeContains(link.source.range, cursorPos)); + if (!link) { + return; + } - const indexOfType = lineText.indexOf(nearestType); - // determine if cursor is on a bracket or paren and if so, return the [content] or (content), skipping over the content range - const cursorOnType = cursorChar === indexOfType || cursorChar === indexOfType + nearestType.length; + if (link.href.kind === HrefKind.Reference && areRangesEqual(link.source.targetRange, link.source.range)) { + return makeSelectionRange(link.source.targetRange, parent); + } - const contentAndNearestType = makeSelectionRange(lsp.Range.create(cursorLine, indexOfType, cursorLine, indexOfType + nearestType.length), linkRange); - const content = makeSelectionRange(lsp.Range.create(cursorLine, indexOfType + 1, cursorLine, indexOfType + nearestType.length - 1), contentAndNearestType); - return cursorOnType ? contentAndNearestType : content; + const fullLinkSelectionRange = makeSelectionRange(link.source.range, parent); + + // determine if cursor is within [text] or (url) in order to know which should be selected + if (rangeContains(link.source.targetRange, cursorPos)) { + // Inside the href. + // Create two ranges, one for the href content and one for the content plus brackets + return makeSelectionRange( + lsp.Range.create( + translatePosition(link.source.targetRange.start, { characterDelta: 1 }), + translatePosition(link.source.targetRange.end, { characterDelta: -1 }), + ), + makeSelectionRange(link.source.targetRange, fullLinkSelectionRange)); + } else { + // Inside the text + return makeSelectionRange( + lsp.Range.create( + translatePosition(link.source.range.start, { characterDelta: 1 }), + translatePosition(link.source.targetRange.start, { characterDelta: -1 }), + ), + fullLinkSelectionRange); } - return undefined; } function isList(token: Token): boolean { @@ -266,5 +293,9 @@ function getFirstChildHeader(document: ITextDocument, header?: TocEntry, toc?: r } function makeSelectionRange(range: lsp.Range, parent: lsp.SelectionRange | undefined): lsp.SelectionRange { + if (parent && areRangesEqual(parent.range, range)) { + return parent; + } + return { range, parent }; } diff --git a/src/languageFeatures/updatePastedLinks.ts b/src/languageFeatures/updatePastedLinks.ts index 3d75554..877b171 100644 --- a/src/languageFeatures/updatePastedLinks.ts +++ b/src/languageFeatures/updatePastedLinks.ts @@ -139,7 +139,7 @@ export class MdUpdatePastedLinksProvider { } let newHrefText = newPathText; - if (link.source.fragmentRange) { + if (link.source.hrefFragmentRange) { newHrefText += '#' + link.href.fragment; } diff --git a/src/test/smartSelect.test.ts b/src/test/smartSelect.test.ts index 544c10e..8142d95 100644 --- a/src/test/smartSelect.test.ts +++ b/src/test/smartSelect.test.ts @@ -13,6 +13,8 @@ import { createNewMarkdownEngine } from './engine'; import { InMemoryWorkspace } from './inMemoryWorkspace'; import { nulLogger } from './nulLogging'; import { CURSOR, getCursorPositions, joinLines } from './util'; +import { MdLinkProvider } from '../languageFeatures/documentLinks'; +import { getLsConfiguration } from '../config'; const testFileName = URI.file('test.md'); @@ -373,7 +375,7 @@ suite('Smart select', () => { `- content 2`, `- content 2`)); - assertNestedLineNumbersEqual(ranges![0], [9, 11], [8, 12], [8, 12], [7, 17], [1, 17], [0, 17]); + assertNestedLineNumbersEqual(ranges![0], [9, 11], [8, 12], [7, 17], [1, 17], [0, 17]); }); test('Smart select fenced code block then list then rest of content on fenced line', async () => { @@ -508,21 +510,21 @@ suite('Smart select', () => { assertNestedRangesEqual(ranges![0], [0, 13, 0, 30], [0, 11, 0, 32], [0, 0, 0, 41]); }); - test('Smart select link', async () => { + test('Smart select link inside href', async () => { const ranges = await getSelectionRangesForDocument(joinLines( `stuff here [text](https${CURSOR}://google.com) and here` )); assertNestedRangesEqual(ranges![0], [0, 18, 0, 46], [0, 17, 0, 47], [0, 11, 0, 47], [0, 0, 0, 56]); }); - test('Smart select brackets', async () => { + test('Smart select link inside text', async () => { const ranges = await getSelectionRangesForDocument(joinLines( `stuff here [te${CURSOR}xt](https://google.com) and here` )); - assertNestedRangesEqual(ranges![0], [0, 12, 0, 26], [0, 11, 0, 27], [0, 11, 0, 47], [0, 0, 0, 56]); + assertNestedRangesEqual(ranges![0], [0, 12, 0, 26], [0, 11, 0, 47], [0, 0, 0, 56]); }); - test('Smart select brackets under header in list', async () => { + test('Smart select link in text under header in list', async () => { const ranges = await getSelectionRangesForDocument(joinLines( `# main header 1`, ``, @@ -533,10 +535,10 @@ suite('Smart select', () => { `- stuff here [te${CURSOR}xt](https://google.com) and here`, `- list` )); - assertNestedRangesEqual(ranges![0], [6, 14, 6, 28], [6, 13, 6, 29], [6, 13, 6, 49], [6, 0, 6, 58], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]); + assertNestedRangesEqual(ranges![0], [6, 14, 6, 28], [6, 13, 6, 49], [6, 0, 6, 58], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]); }); - test('Smart select link under header in list', async () => { + test('Smart select link in href under header in list', async () => { const ranges = await getSelectionRangesForDocument(joinLines( `# main header 1`, ``, @@ -568,14 +570,14 @@ suite('Smart select', () => { const ranges = await getSelectionRangesForDocument(joinLines( `This[extension](https://marketplace.visualstudio.com/items?itemName=meganrogge.template-string-converter) addresses this [requ${CURSOR}est](https://github.com/microsoft/vscode/issues/56704) to convert Javascript/Typescript quotes to backticks when has been entered within a string.` )); - assertNestedRangesEqual(ranges![0], [0, 123, 0, 140], [0, 122, 0, 141], [0, 122, 0, 191], [0, 0, 0, 283]); + assertNestedRangesEqual(ranges![0], [0, 123, 0, 140], [0, 122, 0, 191], [0, 0, 0, 283]); }); test('Smart select bold link', async () => { const ranges = await getSelectionRangesForDocument(joinLines( `**[extens${CURSOR}ion](https://google.com)**` )); - assertNestedRangesEqual(ranges![0], [0, 3, 0, 22], [0, 2, 0, 23], [0, 2, 0, 43], [0, 2, 0, 43], [0, 0, 0, 45], [0, 0, 0, 45]); + assertNestedRangesEqual(ranges![0], [0, 3, 0, 22], [0, 2, 0, 43], [0, 0, 0, 45]); }); test('Smart select inline code block', async () => { @@ -589,28 +591,35 @@ suite('Smart select', () => { const ranges = await getSelectionRangesForDocument(joinLines( `[\`code ${CURSOR} link\`](http://example.com)` )); - assertNestedRangesEqual(ranges![0], [0, 2, 0, 22], [0, 1, 0, 23], [0, 1, 0, 23], [0, 0, 0, 24], [0, 0, 0, 44], [0, 0, 0, 44]); + assertNestedRangesEqual(ranges![0], [0, 2, 0, 22], [0, 1, 0, 23], [0, 0, 0, 44]); + }); + + test('Smart select link in checkbox list', async () => { + const ranges = await getSelectionRangesForDocument(joinLines( + `- [ ] [text${CURSOR}](https://example.com)` + )); + assertNestedRangesEqual(ranges![0], [0, 7, 0, 21], [0, 6, 0, 43], [0, 0, 0, 43]); }); test('Smart select italic', async () => { const ranges = await getSelectionRangesForDocument(joinLines( `*some nice ${CURSOR}text*` )); - assertNestedRangesEqual(ranges![0], [0, 1, 0, 25], [0, 0, 0, 26], [0, 0, 0, 26]); + assertNestedRangesEqual(ranges![0], [0, 1, 0, 25], [0, 0, 0, 26]); }); test('Smart select italic link', async () => { const ranges = await getSelectionRangesForDocument(joinLines( `*[extens${CURSOR}ion](https://google.com)*` )); - assertNestedRangesEqual(ranges![0], [0, 2, 0, 21], [0, 1, 0, 22], [0, 1, 0, 42], [0, 1, 0, 42], [0, 0, 0, 43], [0, 0, 0, 43]); + assertNestedRangesEqual(ranges![0], [0, 2, 0, 21], [0, 1, 0, 42], [0, 0, 0, 43]); }); test('Smart select italic on end', async () => { const ranges = await getSelectionRangesForDocument(joinLines( `*word1 word2 word3${CURSOR}*` )); - assertNestedRangesEqual(ranges![0], [0, 1, 0, 28], [0, 0, 0, 29], [0, 0, 0, 29]); + assertNestedRangesEqual(ranges![0], [0, 1, 0, 28], [0, 0, 0, 29]); }); test('Smart select italic then bold', async () => { @@ -668,9 +677,9 @@ suite('Smart select', () => { function assertNestedLineNumbersEqual(range: lsp.SelectionRange, ...expectedRanges: [number, number][]) { const lineage = getLineage(range); - assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was length: ${lineage.length} values: ${getValues(lineage)}`); + assert.strictEqual(lineage.length, expectedRanges.length, `expected length: ${expectedRanges.length}, but was length: ${lineage.length} values: ${getValues(lineage)}`); for (let i = 0; i < lineage.length; i++) { - assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][1], `parent at a depth of ${i}`); + assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][1], `parent at a depth of ${i}. Expected: ${expectedRanges[i][0]} but was ${lineage[i].range.start.line}`); } } @@ -678,9 +687,9 @@ function assertNestedRangesEqual(range: lsp.SelectionRange, ...expectedRanges: [ const lineage = getLineage(range); assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was length: ${lineage.length}) values: ${getValues(lineage)}`); for (let i = 0; i < lineage.length; i++) { - assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][2], `parent at a depth of ${i}`); - assert(lineage[i].range.start.character === expectedRanges[i][1], `parent at a depth of ${i} on start char`); - assert(lineage[i].range.end.character === expectedRanges[i][3], `parent at a depth of ${i} on end char`); + assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][2], `parent at a depth of ${i}. Expected: ${expectedRanges[i][0]} but was ${lineage[i].range.start.line}`); + assert(lineage[i].range.start.character === expectedRanges[i][1], `parent at a depth of ${i} on start char. Expected: ${expectedRanges[i][1]} but was ${lineage[i].range.start.character}`); + assert(lineage[i].range.end.character === expectedRanges[i][3], `parent at a depth of ${i} on end char. Expected: ${expectedRanges[i][3]} but was ${lineage[i].range.end.character}`); } } @@ -706,10 +715,15 @@ function assertLineNumbersEqual(selectionRange: lsp.SelectionRange, startLine: n } function getSelectionRangesForDocument(contents: string, pos?: lsp.Position[]): Promise { + const config = getLsConfiguration({}); + const doc = new InMemoryDocument(testFileName, contents); const workspace = new InMemoryWorkspace([doc]); const engine = createNewMarkdownEngine(); - const provider = new MdSelectionRangeProvider(engine, new MdTableOfContentsProvider(engine, workspace, nulLogger), nulLogger); + const tocProvider = new MdTableOfContentsProvider(engine, workspace, nulLogger); + const linkProvider = new MdLinkProvider(config, engine, workspace, tocProvider, nulLogger); + + const provider = new MdSelectionRangeProvider(engine, tocProvider, linkProvider, nulLogger); const positions = pos ? pos : getCursorPositions(contents, doc); return provider.provideSelectionRanges(doc, positions, new lsp.CancellationTokenSource().token); } diff --git a/src/types/documentLink.ts b/src/types/documentLink.ts index 8ee91f9..f02a55e 100644 --- a/src/types/documentLink.ts +++ b/src/types/documentLink.ts @@ -62,7 +62,7 @@ export interface MdLinkSource { * * For `[boris](/cat.md#siberian "title")` this would be `/cat.md` */ - readonly pathText: string; + readonly hrefPathText: string; /** * The range of the path in this link. @@ -78,7 +78,7 @@ export interface MdLinkSource { * * For `[boris](/cat.md#siberian "title")` this would be the range of `#siberian` */ - readonly fragmentRange: lsp.Range | undefined; + readonly hrefFragmentRange: lsp.Range | undefined; readonly isAngleBracketLink: boolean; }