Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate more minimal edit when updating links on paste #174

Merged
merged 1 commit into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading