Skip to content

CodeMapper support #55406

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

Merged
merged 15 commits into from
May 22, 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: 2 additions & 0 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,8 @@ export class SessionClient implements LanguageService {
});
}

mapCode = notImplemented;

private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs {
return typeof positionOrRange === "number"
? this.createFileLocationRequestArgs(fileName, positionOrRange)
Expand Down
51 changes: 51 additions & 0 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4510,6 +4510,57 @@ export class TestState {

this.verifyCurrentFileContent(newFileContent);
}

public baselineMapCode(
ranges: Range[][],
changes: string[] = [],
): void {
const fileName = this.activeFile.fileName;
const focusLocations = ranges.map(r =>
r.map(({ pos, end }) => {
return { start: pos, length: end - pos };
})
);
let before = this.getFileContent(fileName);
const edits = this.languageService.mapCode(
fileName,
// We trim the leading whitespace stuff just so our test cases can be more readable.
changes,
focusLocations,
this.formatCodeSettings,
{},
);
this.applyChanges(edits);
focusLocations.forEach(r => {
r.sort((a, b) => a.start - b.start);
});
focusLocations.sort((a, b) => a[0].start - b[0].start);
for (const subLoc of focusLocations) {
for (const { start, length } of subLoc) {
let offset = 0;
for (const sl2 of focusLocations) {
for (const { start: s2, length: l2 } of sl2) {
if (s2 < start) {
offset += 4;
if ((s2 + l2) > start) {
offset -= 2;
}
}
}
}
before = before.slice(0, start + offset) + "[|" + before.slice(start + offset, start + offset + length) + "|]" + before.slice(start + offset + length);
}
}
const after = this.getFileContent(fileName);
const baseline = `
// === ORIGINAL ===
${before}
// === INCOMING CHANGES ===
${changes.join("\n// ---\n")}
// === MAPPED ===
${after}`;
this.baseline("mapCode", baseline, ".mapCode.ts");
}
}

function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: readonly ts.TextChange[]): ts.TextRange {
Expand Down
4 changes: 4 additions & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ export class VerifyNegatable {
public uncommentSelection(newFileContent: string) {
this.state.uncommentSelection(newFileContent);
}

public baselineMapCode(ranges: FourSlash.Range[][], changes: string[] = []): void {
this.state.baselineMapCode(ranges, changes);
}
}

export class Verify extends VerifyNegatable {
Expand Down
33 changes: 33 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export const enum CommandTypes {
ProvideCallHierarchyOutgoingCalls = "provideCallHierarchyOutgoingCalls",
ProvideInlayHints = "provideInlayHints",
WatchChange = "watchChange",
MapCode = "mapCode",
}

/**
Expand Down Expand Up @@ -2342,6 +2343,38 @@ export interface InlayHintsResponse extends Response {
body?: InlayHintItem[];
}

export interface MapCodeRequestArgs extends FileRequestArgs {
/**
* The files and changes to try and apply/map.
*/
mapping: MapCodeRequestDocumentMapping;
}

export interface MapCodeRequestDocumentMapping {
/**
* The specific code to map/insert/replace in the file.
*/
contents: string[];

/**
* Areas of "focus" to inform the code mapper with. For example, cursor
* location, current selection, viewport, etc. Nested arrays denote
* priority: toplevel arrays are more important than inner arrays, and
* inner array priorities are based on items within that array. Items
* earlier in the arrays have higher priority.
*/
focusLocations?: TextSpan[][];
}

export interface MapCodeRequest extends FileRequest {
command: CommandTypes.MapCode;
arguments: MapCodeRequestArgs;
}

export interface MapCodeResponse extends Response {
body: readonly FileCodeEdits[];
}

/**
* Synchronous request for semantic diagnostics of one file.
*/
Expand Down
23 changes: 23 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1906,6 +1906,26 @@ export class Session<TMessage = string> implements EventSender {
});
}

private mapCode(args: protocol.MapCodeRequestArgs): protocol.FileCodeEdits[] {
const formatOptions = this.getHostFormatOptions();
const preferences = this.getHostPreferences();
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
const focusLocations = args.mapping.focusLocations?.map(spans => {
return spans.map(loc => {
const start = scriptInfo.lineOffsetToPosition(loc.start.line, loc.start.offset);
const end = scriptInfo.lineOffsetToPosition(loc.end.line, loc.end.offset);
return {
start,
length: end - start,
};
});
});

const changes = languageService.mapCode(file, args.mapping.contents, focusLocations, formatOptions, preferences);
return this.mapTextChangesToCodeEdits(changes);
}

private setCompilerOptionsForInferredProjects(args: protocol.SetCompilerOptionsForInferredProjectsArgs): void {
this.projectService.setCompilerOptionsForInferredProjects(args.options, args.projectRootPath);
}
Expand Down Expand Up @@ -3610,6 +3630,9 @@ export class Session<TMessage = string> implements EventSender {
[protocol.CommandTypes.ProvideInlayHints]: (request: protocol.InlayHintsRequest) => {
return this.requiredResponse(this.provideInlayHints(request.arguments));
},
[protocol.CommandTypes.MapCode]: (request: protocol.MapCodeRequest) => {
return this.requiredResponse(this.mapCode(request.arguments));
},
}));

public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) {
Expand Down
3 changes: 3 additions & 0 deletions src/services/_namespaces/ts.MapCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* Generated file to emulate the ts.MapCode namespace. */

export * from "../mapCode.js";
2 changes: 2 additions & 0 deletions src/services/_namespaces/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export { GoToDefinition };
import * as InlayHints from "./ts.InlayHints.js";
export { InlayHints };
import * as JsDoc from "./ts.JsDoc.js";
import * as MapCode from "./ts.MapCode.js";
export { MapCode };
export { JsDoc };
import * as NavigateTo from "./ts.NavigateTo.js";
export { NavigateTo };
Expand Down
Loading