diff --git a/rascal-vscode-extension/package.json b/rascal-vscode-extension/package.json index 02a260711..3352b0693 100644 --- a/rascal-vscode-extension/package.json +++ b/rascal-vscode-extension/package.json @@ -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", diff --git a/rascal-vscode-extension/src/RascalExtension.ts b/rascal-vscode-extension/src/RascalExtension.ts index b85751374..318f8ea32 100644 --- a/rascal-vscode-extension/src/RascalExtension.ts +++ b/rascal-vscode-extension/src/RascalExtension.ts @@ -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; @@ -51,6 +52,8 @@ export class RascalExtension implements vscode.Disposable { this.registerMainRun(); this.registerImportModule(); + this.registerCopyLocation(); + vscode.window.registerTerminalLinkProvider(new RascalTerminalLinkProvider(this.rascal.rascalClient)); } @@ -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, diff --git a/rascal-vscode-extension/src/RascalTerminalLinkProvider.ts b/rascal-vscode-extension/src/RascalTerminalLinkProvider.ts index 95fafce84..f966e38b3 100644 --- a/rascal-vscode-extension/src/RascalTerminalLinkProvider.ts +++ b/rascal-vscode-extension/src/RascalTerminalLinkProvider.ts @@ -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]; @@ -78,7 +79,7 @@ export class RascalTerminalLinkProvider implements TerminalLinkProvider 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; + 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]; + } + + /** + * 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; + + for (let i = 0; i < columnVSCode - 1; i++) { + const c = fullLine.charCodeAt(i); + if (PositionConverter.isHighSurrogate(c) && PositionConverter.isLowSurrogate(fullLine.charCodeAt(i + 1))) { + 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; + for (let i = 0; i < endOffset - 1; i++) { + const c = fullText.charCodeAt(i); + if (PositionConverter.isHighSurrogate(c) && PositionConverter.isLowSurrogate(fullText.charCodeAt(i + 1))) { + if (i <= offset) { + offset--; + } + newEndOffset--; + i++; // the following letter is known to be the low surrogate -> we can skip it + } + } + return [offset, newEndOffset]; + } + + /** + * 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); + } +}