Skip to content

Commit

Permalink
Merge pull request #174 from mjbvz/dev/mjbvz/paste-links-min-edit
Browse files Browse the repository at this point in the history
Generate more minimal edit when updating links on paste
  • Loading branch information
mjbvz authored Apr 2, 2024
2 parents a7adcad + 9391cbe commit 3ad8348
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 84 deletions.
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export function createLanguageService(init: LanguageServiceInitialization): IMdL
const workspaceSymbolProvider = new MdWorkspaceSymbolProvider(init.workspace, docSymbolProvider);
const organizeLinkDefinitions = new MdOrganizeLinkDefinitionProvider(linkProvider);
const documentHighlightProvider = new MdDocumentHighlightProvider(config, tocProvider, linkProvider);
const rewritePastedLinksProvider = new MdUpdatePastedLinksProvider(init.parser, init.workspace);
const rewritePastedLinksProvider = new MdUpdatePastedLinksProvider(linkProvider);

const extractCodeActionProvider = new MdExtractLinkDefinitionCodeActionProvider(linkProvider);
const removeLinkDefinitionActionProvider = new MdRemoveLinkDefinitionCodeActionProvider();
Expand Down
22 changes: 13 additions & 9 deletions src/languageFeatures/documentLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,7 @@ export class MdLinkProvider extends Disposable {
readonly #config: LsConfiguration;
readonly #workspace: IWorkspace;
readonly #tocProvider: MdTableOfContentsProvider;
readonly #logger: ILogger;

constructor(
config: LsConfiguration,
Expand All @@ -819,23 +820,26 @@ export class MdLinkProvider extends Disposable {
this.#config = config;
this.#workspace = workspace;
this.#tocProvider = tocProvider;
this.#logger = logger;

this.#linkComputer = new MdLinkComputer(tokenizer, this.#workspace);
this.#linkCache = this._register(new MdDocumentInfoCache(this.#workspace, async (doc, token) => {
logger.log(LogLevel.Debug, 'LinkProvider.compute', { document: doc.uri, version: doc.version });

const links = await this.#linkComputer.getAllLinks(doc, token);
return {
links,
definitions: new LinkDefinitionSet(links),
};
}));
this.#linkCache = this._register(new MdDocumentInfoCache(this.#workspace, (doc, token) => this.getLinksWithoutCaching(doc, token)));
}

public getLinks(document: ITextDocument): Promise<MdDocumentLinksInfo> {
return this.#linkCache.getForDocument(document);
}

public async getLinksWithoutCaching(doc: ITextDocument, token: lsp.CancellationToken): Promise<MdDocumentLinksInfo> {
this.#logger.log(LogLevel.Debug, 'LinkProvider.compute', { document: doc.uri, version: doc.version });

const links = await this.#linkComputer.getAllLinks(doc, token);
return {
links,
definitions: new LinkDefinitionSet(links),
};
}

public async provideDocumentLinks(document: ITextDocument, token: lsp.CancellationToken): Promise<lsp.DocumentLink[]> {
const { links, definitions } = await this.getLinks(document);
if (token.isCancellationRequested) {
Expand Down
4 changes: 2 additions & 2 deletions src/languageFeatures/rename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ export class MdRenameProvider {
}

if (doc) {
const editedDoc = new InMemoryDocument(URI.parse(existingHeader.location.uri), doc.getText());
editedDoc.updateContent(editedDoc.applyEdits([lsp.TextEdit.replace(existingHeader.location.range, '# ' + newHeaderText)]));
const editedDoc = new InMemoryDocument(URI.parse(existingHeader.location.uri), doc.getText())
.applyEdits([lsp.TextEdit.replace(existingHeader.location.range, '# ' + newHeaderText)]);

const [oldToc, newToc] = await Promise.all([
this.#tableOfContentProvider.getForDocument(doc),
Expand Down
89 changes: 60 additions & 29 deletions src/languageFeatures/updatePastedLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import * as lsp from 'vscode-languageserver-protocol';
import { URI } from 'vscode-uri';
import { IMdParser } from '../parser';
import { InMemoryDocument } from '../types/inMemoryDocument';
import { isBefore, isBeforeOrEqual } from '../types/position';
import { rangeContains } from '../types/range';
import { getDocUri, ITextDocument } from '../types/textDocument';
import { computeRelativePath } from '../util/path';
import { IWorkspace } from '../workspace';
import { createAddDefinitionEdit } from './codeActions/extractLinkDef';
import { HrefKind, LinkDefinitionSet, MdLinkComputer, MdLinkDefinition } from './documentLinks';
import { HrefKind, LinkDefinitionSet, MdLinkDefinition, MdLinkProvider } from './documentLinks';

class PasteLinksCopyMetadata {

Expand All @@ -35,22 +34,21 @@ class PasteLinksCopyMetadata {

export class MdUpdatePastedLinksProvider {

readonly #linkComputer: MdLinkComputer;
readonly #linkProvider: MdLinkProvider;

constructor(
tokenizer: IMdParser,
workspace: IWorkspace,
linkProvider: MdLinkProvider,
) {
this.#linkComputer = new MdLinkComputer(tokenizer, workspace);
this.#linkProvider = linkProvider;
}

async prepareDocumentPaste(document: ITextDocument, _ranges: readonly lsp.Range[], token: lsp.CancellationToken): Promise<string> {
const links = await this.#linkComputer.getAllLinks(document, token);
const linkInfo = await this.#linkProvider.getLinks(document);
if (token.isCancellationRequested) {
return '';
}

const metadata = new PasteLinksCopyMetadata(getDocUri(document), new LinkDefinitionSet(links));
const metadata = new PasteLinksCopyMetadata(getDocUri(document), linkInfo.definitions);
return metadata.toJSON();
}

Expand Down Expand Up @@ -80,17 +78,16 @@ export class MdUpdatePastedLinksProvider {
// Find the links in the pasted text by applying the paste edits to an in-memory document.
// Use `copySource` as the doc uri to make sure links are resolved in its context
const editedDoc = new InMemoryDocument(metadata.source, targetDocument.getText());
editedDoc.updateContent(editedDoc.applyEdits(sortedPastes));
editedDoc.replaceContents(editedDoc.previewEdits(sortedPastes));

const allLinks = await this.#linkComputer.getAllLinks(editedDoc, token);
const allLinks = await this.#linkProvider.getLinksWithoutCaching(editedDoc, token);
if (token.isCancellationRequested) {
return;
}

const pastedRanges = this.#computedPastedRanges(sortedPastes, targetDocument, editedDoc);

const currentDefinitionSet = new LinkDefinitionSet(allLinks);
const linksToRewrite = allLinks
const linksToRewrite = allLinks.links
// We only rewrite relative links and references
.filter(link => {
if (link.href.kind === HrefKind.Reference) {
Expand Down Expand Up @@ -119,7 +116,7 @@ export class MdUpdatePastedLinksProvider {
}

// If there's an existing definition with the same exact ref, we don't need to add it again
if (currentDefinitionSet.lookup(link.href.ref)?.source.hrefText === originalRef.source.hrefText) {
if (allLinks.definitions.lookup(link.href.ref)?.source.hrefText === originalRef.source.hrefText) {
continue;
}

Expand All @@ -136,29 +133,59 @@ export class MdUpdatePastedLinksProvider {
newHrefText += '#' + link.href.fragment;
}

if (link.source.hrefText !== newHrefText) {
if (link.source.hrefText !== newHrefText) {
rewriteLinksEdits.push(lsp.TextEdit.replace(link.source.hrefRange, newHrefText));
}
}
}

// Plus add an edit that inserts new definitions
if (newDefinitionsToAdd.length) {
rewriteLinksEdits.push(createAddDefinitionEdit(editedDoc, [...currentDefinitionSet], newDefinitionsToAdd.map(def => ({ placeholder: def.ref.text, definitionText: def.source.hrefText }))));
}

// If nothing was rewritten we can just use normal text paste.
if (!rewriteLinksEdits.length) {
// If nothing was rewritten we can just use normal text paste
if (!rewriteLinksEdits.length && !newDefinitionsToAdd.length) {
return;
}

// Generate the final edits by grabbing text from the edited document
const finalDoc = new InMemoryDocument(editedDoc.$uri, editedDoc.applyEdits(rewriteLinksEdits));
// Generate a minimal set of edits for the pastes
const outEdits: lsp.TextEdit[] = [];
const finalDoc = new InMemoryDocument(editedDoc.$uri, editedDoc.previewEdits(rewriteLinksEdits));

let offsetAdjustment = 0;
for (let i = 0; i < pastedRanges.length; ++i) {
const pasteRange = pastedRanges[i];
const originalPaste = sortedPastes[i];

// Adjust the range to account for the `rewriteLinksEdits`
for (
let edit: lsp.TextEdit | undefined;
(edit = rewriteLinksEdits[0]) && isBefore(edit.range.start, pasteRange.start);
rewriteLinksEdits.shift()
) {
offsetAdjustment += computeEditLengthChange(edit, editedDoc);
}
const startOffset = editedDoc.offsetAt(pasteRange.start) + offsetAdjustment;

for (
let edit: lsp.TextEdit | undefined;
(edit = rewriteLinksEdits[0]) && isBeforeOrEqual(edit.range.end, pasteRange.end);
rewriteLinksEdits.shift()
) {
offsetAdjustment += computeEditLengthChange(edit, editedDoc);
}
const endOffset = editedDoc.offsetAt(pasteRange.end) + offsetAdjustment;

const range = lsp.Range.create(finalDoc.positionAt(startOffset), finalDoc.positionAt(endOffset));
outEdits.push(lsp.TextEdit.replace(originalPaste.range, finalDoc.getText(range)));
}

// TODO: generate more minimal edit
return [
lsp.TextEdit.replace(lsp.Range.create(0, 0, 100_000, 0), finalDoc.getText()),
];
// Add an edit that inserts new definitions
if (newDefinitionsToAdd.length) {
const targetLinks = await this.#linkProvider.getLinks(targetDocument);
if (token.isCancellationRequested) {
return;
}
outEdits.push(createAddDefinitionEdit(targetDocument, Array.from(targetLinks.definitions), newDefinitionsToAdd.map(def => ({ placeholder: def.ref.text, definitionText: def.source.hrefText }))));
}

return outEdits;
}

#parseMetadata(rawCopyMetadata: string): PasteLinksCopyMetadata | undefined {
Expand Down Expand Up @@ -186,4 +213,8 @@ export class MdUpdatePastedLinksProvider {

return pastedRanges;
}
}
}

function computeEditLengthChange(edit: lsp.TextEdit, editedDoc: InMemoryDocument) {
return edit.newText.length - (editedDoc.offsetAt(edit.range.end) - editedDoc.offsetAt(edit.range.start));
}
2 changes: 1 addition & 1 deletion src/test/diagnostic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ suite('Diagnostic Manager', () => {
assert.strictEqual(workspace.statCallList.length, 1);

// Edit doc
doc1.updateContent(joinLines(
doc1.replaceContents(joinLines(
`![i](/nosuch.png)`,
`[ref]`,
`[ref]: http://example.com`
Expand Down
22 changes: 11 additions & 11 deletions src/test/organizeLinkDef.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ suite('Organize link definitions', () => {
`[a]: http://example.com`,
));
const edits = await getOrganizeEdits(store, doc);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
`[a]: http://example.com`,
`[b]: http://example.com`,
Expand All @@ -62,7 +62,7 @@ suite('Organize link definitions', () => {
`y`,
));
const edits = await getOrganizeEdits(store, doc);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
`x`,
``,
Expand All @@ -80,7 +80,7 @@ suite('Organize link definitions', () => {
``,
));
const edits = await getOrganizeEdits(store, doc);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
`x`,
``,
Expand All @@ -98,7 +98,7 @@ suite('Organize link definitions', () => {
`[a]: http://example.com`,
));
const edits = await getOrganizeEdits(store, doc);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
`x`,
``,
Expand Down Expand Up @@ -127,7 +127,7 @@ suite('Organize link definitions', () => {
`[GitHub Issue Tracker]: https://github.com/microsoft/vscode-mssql/issues`,
));
const edits = await getOrganizeEdits(store, doc);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
`* [SQL Server documentation]`,
`* [SQL Server on Linux documentation]`,
Expand Down Expand Up @@ -156,7 +156,7 @@ suite('Organize link definitions', () => {
``,
));
const edits = await getOrganizeEdits(store, doc);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
`x`,
``,
Expand All @@ -181,7 +181,7 @@ suite('Organize link definitions', () => {
));

const edits = await getOrganizeEdits(store, doc);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
``,
`a`,
Expand Down Expand Up @@ -215,7 +215,7 @@ suite('Organize link definitions', () => {
));

const edits = await getOrganizeEdits(store, doc);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
``,
`a`,
Expand Down Expand Up @@ -244,7 +244,7 @@ suite('Organize link definitions', () => {
`z`,
));
const edits = await getOrganizeEdits(store, doc);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
`x`,
``,
Expand All @@ -265,7 +265,7 @@ suite('Organize link definitions', () => {
`[b]: http://example.com`,
));
const edits = await getOrganizeEdits(store, doc, /* removeUnused */ true);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
`text [b] text`,
``,
Expand All @@ -282,7 +282,7 @@ suite('Organize link definitions', () => {
`[a]: http://example.com?a`,
));
const edits = await getOrganizeEdits(store, doc, /* removeUnused */ true);
const newContent = doc.applyEdits(edits);
const newContent = doc.previewEdits(edits);
assert.deepStrictEqual(newContent, joinLines(
`text [a] text [link][c]`,
``,
Expand Down
Loading

0 comments on commit 3ad8348

Please sign in to comment.