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

Add command to create rascal location from selection #283

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 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
17 changes: 17 additions & 0 deletions rascal-vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,25 @@
{
"command": "rascalmpl.importMain",
"title": "Start Rascal Terminal and Import this module"
},
{
"command": "rascalmpl.copyLocation",
"title": "Copy Location of Selection to Clipboard"
}
],
"keybindings": [
{
"command": "rascalmpl.copyLocation",
"key": "shift+alt+l",
"mac": "shift+cmd+l"
}
],
"menus": {
"editor/context": [{
"command": "rascalmpl.copyLocation",
"group": "editor/context"
}]
},
"languages": [
{
"id": "rascalmpl",
Expand Down
33 changes: 33 additions & 0 deletions rascal-vscode-extension/src/RascalExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { RascalLanguageServer } from './lsp/RascalLanguageServer';
import { LanguageParameter, ParameterizedLanguageServer } from './lsp/ParameterizedLanguageServer';
import { RascalTerminalLinkProvider } from './RascalTerminalLinkProvider';
import { VSCodeUriResolverServer } from './fs/VSCodeURIResolver';
import { PositionConverter } from './util/PositionConverter';

export class RascalExtension implements vscode.Disposable {
private readonly vfsServer: VSCodeUriResolverServer;
Expand All @@ -51,6 +52,8 @@ export class RascalExtension implements vscode.Disposable {
this.registerMainRun();
this.registerImportModule();

this.registerCopyLocation();

vscode.window.registerTerminalLinkProvider(new RascalTerminalLinkProvider(this.rascal.rascalClient));
}

Expand Down Expand Up @@ -99,6 +102,36 @@ export class RascalExtension implements vscode.Disposable {
);
}


private registerCopyLocation() {
this.context.subscriptions.push(
vscode.commands.registerTextEditorCommand("rascalmpl.copyLocation", (editor, _edit) => {
if (editor) {
const uri = editor.document.uri.toString();
const selection = editor.selection;

const offset = editor.document.offsetAt(selection.start);
const length = editor.document.getText(selection).length;
const [offsetUtf32, lengthUtf32] = PositionConverter.vsCodeToRascalOffsetLength(editor.document, offset, length);

// the startline is not affected by UTF16 or UTF32, because line breaks are always encoded as a single character
const startLine = selection.start.line;
const endLine = selection.end.line;

const startColumn = selection.start.character;
const startColumnUtf32 = PositionConverter.vsCodeToRascalColumn(editor.document, startLine, startColumn);
const endColumn = selection.end.character;
const endColumnUtf32 = PositionConverter.vsCodeToRascalColumn(editor.document, endLine, endColumn);

const location = `|${uri}|(${offsetUtf32},${lengthUtf32},<${startLine+1},${startColumnUtf32}>,<${endLine+1},${endColumnUtf32}>)`;


vscode.env.clipboard.writeText(location);
}
})
);
}

private startTerminal(uri: vscode.Uri | undefined, ...extraArgs: string[]) {
return vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
Expand Down
70 changes: 3 additions & 67 deletions rascal-vscode-extension/src/RascalTerminalLinkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@
import * as vscode from 'vscode';
import { CancellationToken, ProviderResult, TerminalLink, TerminalLinkContext, TerminalLinkProvider } from 'vscode';
import { LanguageClient } from 'vscode-languageclient/node';
import { PositionConverter } from './util/PositionConverter';

interface ExtendedLink extends TerminalLink {
loc: SourceLocation;
}

interface SourceLocation {
export interface SourceLocation {
uri: string;
offsetLength?: [number, number];
beginLineColumn?: [number, number];
Expand Down Expand Up @@ -78,7 +79,7 @@ export class RascalTerminalLinkProvider implements TerminalLinkProvider<Extended
const td = await vscode.workspace.openTextDocument(vscode.Uri.parse(rsloc.uri));
const te = await vscode.window.showTextDocument(td);

const targetRange = translateRange(rsloc, td);
const targetRange = PositionConverter.rascalToVSCodeRange(td, rsloc);
if (targetRange) {
te.revealRange(targetRange);
te.selection = new vscode.Selection(
Expand All @@ -91,27 +92,6 @@ export class RascalTerminalLinkProvider implements TerminalLinkProvider<Extended
}
}

function translateRange(sloc: SourceLocation, td: vscode.TextDocument): vscode.Range | undefined {
if (sloc.beginLineColumn && sloc.endLineColumn) {
const beginLine = sloc.beginLineColumn[0] - 1;
const endLine = sloc.endLineColumn[0] - 1;
return new vscode.Range(
beginLine,
fixedColumn(td, beginLine, sloc.beginLineColumn[1]),
endLine,
fixedColumn(td, endLine, sloc.endLineColumn[1]),
);
}
else if (sloc.offsetLength) {
const rangePositions = fixedOffsetLengthPositions(td, sloc.offsetLength[0], sloc.offsetLength[1]);
return new vscode.Range(
td.positionAt(rangePositions[0]),
td.positionAt(rangePositions[1])
);
}
return undefined;
}

function buildLink(match: RegExpExecArray): ExtendedLink {
const linkMatch = match[0];
const linkOffset = match.index + 1;
Expand All @@ -133,47 +113,3 @@ function buildLink(match: RegExpExecArray): ExtendedLink {
loc: sloc
};
}

// from https://github.com/microsoft/vscode/blob/main/src/vs/base/common/strings.ts
function isHighSurrogate(charCode: number): boolean {
return (0xD800 <= charCode && charCode <= 0xDBFF);
}

// from https://github.com/microsoft/vscode/blob/main/src/vs/base/common/strings.ts
function isLowSurrogate(charCode: number): boolean {
return (0xDC00 <= charCode && charCode <= 0xDFFF);
}


/**
* locate surrogate pairs on the current line, and if present, offset rascal columns by the surrogate pairs before it
*/
function fixedColumn(td: vscode.TextDocument, line: number, originalColumn: number): number {
const fullLine = td.lineAt(line).text;
let result = originalColumn;
for (let i = 0; i < fullLine.length && i < result; i++) {
const c = fullLine.charCodeAt(i);
if (isHighSurrogate(c) && (i + 1) < fullLine.length && isLowSurrogate(fullLine.charCodeAt(i + 1))) {
i++;
result++;
}
}
return result;
}


function fixedOffsetLengthPositions(td: vscode.TextDocument, offset: number, length: number): [number, number] {
const fullText = td.getText();
let endOffset = offset+length;
for (let i = 0; i < fullText.length && i < endOffset; i++) {
const c = fullText.charCodeAt(i);
if (isHighSurrogate(c) && (i + 1) < fullText.length && isLowSurrogate(fullText.charCodeAt(i + 1))) {
if (i <= offset) {
offset++;
}
endOffset++;
i++;
}
}
return [offset, endOffset];
}
189 changes: 189 additions & 0 deletions rascal-vscode-extension/src/util/PositionConverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import * as vscode from "vscode";
import { SourceLocation } from '../RascalTerminalLinkProvider';

/**
* This class allows to convert text locations between VS Code and Rascal.
* Some characters, which VS Code counts as two characters because it uses UTF-16 encoding,
* are only a single character for Rascal, which uses UTF-32 encoding.
*
* Assuming UTF-16 encoding, because this code is executed in TypeScript,
* all methods in this class therefore iterate character-wise over a text to find pairs of characters
* that would be encoded as a single character in UTF-32.
*
* If they find such a pair, they change the position to encount for the difference in encoding:
* If the position comes from Rascal, it is too short and must be increased by 1.
* If the position comes from VS Code, it is too long and must be decreased by 1.
*/
export class PositionConverter {

/***************************************
* Rascal -> VS Code *
***************************************/

/**
* Converts the column position from Rascal (UTF-32) to VS Code (UTF-16).
* @param td the text document where the column information is located in.
* @param line the line in which the column to be changed is located.
* @param rascalColumn the column as given by Rascal.
* @returns the column as understood by VS Code.
*/
static rascalToVSCodeColumn(td: vscode.TextDocument, line: number, rascalColumn: number): number {
const fullLine = td.lineAt(line).text;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be line - 1? (and maybe named rascalLine?) Or should we document that we assume the caller already has converted the line number to vs code?

let result = rascalColumn;
for (let i = 0; i < fullLine.length && i < result; i++) {
const c = fullLine.charCodeAt(i);
if (PositionConverter.isHighSurrogate(c) && (i + 1) < fullLine.length && PositionConverter.isLowSurrogate(fullLine.charCodeAt(i + 1))) {
i++;
result++;
}
}
return result;
}

/**
* Converts the offset and length position from Rascal (UTF-32) to VS Code (UTF-16).
* @param td the text document where the information is located in.
* @param offset the offset as given by Rascal.
* @param length the length as given by Rascal.
* @returns the offset and length as understood by VS Code.
*/
static rascalToVSCodeOffsetLength(td: vscode.TextDocument, offset: number, length: number): [number, number] {
const fullText = td.getText();
let endOffset = offset + length;
for (let i = 0; i < fullText.length && i < endOffset; i++) {
const c = fullText.charCodeAt(i);
if (PositionConverter.isHighSurrogate(c) && (i + 1) < fullText.length && PositionConverter.isLowSurrogate(fullText.charCodeAt(i + 1))) {
if (i <= offset) { // the character comes before the offset, so it must shift the offset
offset++;
}
endOffset++;
i++;
}
}
return [offset, endOffset];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has a bug, as the endOffset is not the length. Maybe should it be endOffset - offset ? or we should make it clear it returns 2 positions.

}

/**
* Converts a range from Rascal (UTF-32) to VS Code (UTF-16).
* A range is given in the form of a SourceLocation which can encode a range
* either as an offset and length or using pairs of line and column
* for begin and end of the range.
* @param td the text document where the information is located in.
* @param sloc a source location as given by Rascal.
* @returns the range as understood by VS Code or `undefined`, if the range is not specified correctly.
*/
static rascalToVSCodeRange(td: vscode.TextDocument, sloc: SourceLocation): vscode.Range | undefined {
if (sloc.beginLineColumn && sloc.endLineColumn) {
const beginLine = sloc.beginLineColumn[0] - 1;
const endLine = sloc.endLineColumn[0] - 1;
return new vscode.Range(
beginLine,
PositionConverter.rascalToVSCodeColumn(td, beginLine, sloc.beginLineColumn[1]),
endLine,
PositionConverter.rascalToVSCodeColumn(td, endLine, sloc.endLineColumn[1])
);
}
else if (sloc.offsetLength) {
const rangePositions = PositionConverter.rascalToVSCodeOffsetLength(td, sloc.offsetLength[0], sloc.offsetLength[1]);
return new vscode.Range(
td.positionAt(rangePositions[0]),
td.positionAt(rangePositions[1])
);
}
return undefined;
}

/***************************************
* VS Code -> Rascal *
***************************************/

/**
* Converts the column position from VS Code (UTF-16) to Rascal (UTF-32).
* @param td the text document where the column information is located in.
* @param line the line in which the column to be changed is located.
* @param columnVSCode the column as given by VS Code.
* @returns the column as understood by Rascal.
*/
static vsCodeToRascalColumn(td: vscode.TextDocument, line: number, columnVSCode: number): number {
const fullLine = td.lineAt(line).text;
let lengthRascal = columnVSCode;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NP: should it be called columnRascal?


for (let i = 0; i < columnVSCode - 1; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should make sure that columnVSCode is not more than fullLine.length()

const c = fullLine.charCodeAt(i);
if (PositionConverter.isHighSurrogate(c) && PositionConverter.isLowSurrogate(fullLine.charCodeAt(i + 1))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will throw exception if (i + 1) == length.

lengthRascal--;
i++; // the following letter is known to be the low surrogate -> we can skip it
}
}

return lengthRascal;
}

/**
* Converts the offset and length position from VS Code (UTF-16) to Rascal (UTF-32).
* @param td the text document where the information is located in.
* @param offset the offset as given by VS Code.
* @param length the length as given by VS Code.
* @returns the offset and length as understood by Rascal.
*/
static vsCodeToRascalOffsetLength(td: vscode.TextDocument, offset: number, length: number): [number, number] {
const fullText = td.getText();
const endOffset = offset + length;
let newEndOffset = endOffset;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be called newLength? and just be newLength = length?

for (let i = 0; i < endOffset - 1; i++) {
const c = fullText.charCodeAt(i);
if (PositionConverter.isHighSurrogate(c) && PositionConverter.isLowSurrogate(fullText.charCodeAt(i + 1))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, same bug, i+1 might throw exception in case of malformed string.

if (i <= offset) {
offset--;
}
newEndOffset--;
i++; // the following letter is known to be the low surrogate -> we can skip it
}
}
return [offset, newEndOffset];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not return 2 positions, but 1 offset, and one length.

}

/**
* Converts a range from VS Code (UTF-16) to Rascal (UTF-32).
* A range is given in the form of a SourceLocation which can encode a range
* either as an offset and length or using pairs of line and column
* for begin and end of the range.
* @param td the text document where the information is located in.
* @param sloc a source location as given by VS Code.
* @returns the range as understood by Rascal or `undefined`, if the range is not specified correctly.
*/
static vsCodeToRascalRange(td: vscode.TextDocument, sloc: SourceLocation): vscode.Range | undefined {
if (sloc.beginLineColumn && sloc.endLineColumn) {
const beginLine = sloc.beginLineColumn[0];
const endLine = sloc.endLineColumn[0];
return new vscode.Range(
beginLine,
PositionConverter.vsCodeToRascalColumn(td, beginLine, sloc.beginLineColumn[1]),
endLine,
PositionConverter.vsCodeToRascalColumn(td, endLine, sloc.endLineColumn[1])
);
}
else if (sloc.offsetLength) {
const rangePositions = PositionConverter.vsCodeToRascalOffsetLength(td, sloc.offsetLength[0], sloc.offsetLength[1]);
return new vscode.Range(
td.positionAt(rangePositions[0]),
td.positionAt(rangePositions[1])
);
}
return undefined;
}

/***************************************
* Util *
***************************************/

// from https://github.com/microsoft/vscode/blob/main/src/vs/base/common/strings.ts
static isHighSurrogate(charCode: number): boolean {
return (55296 <= charCode && charCode <= 56319);
}

// from https://github.com/microsoft/vscode/blob/main/src/vs/base/common/strings.ts
static isLowSurrogate(charCode: number): boolean {
return (56320 <= charCode && charCode <= 57343);
}
}
Loading