diff --git a/package.json b/package.json index eee8d383..e6c89a15 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,11 @@ "title": "Run Deephaven File", "icon": "$(run-all)" }, + { + "command": "vscode-deephaven.runMarkdownCodeblock", + "title": "Run Deephaven Markdown Codeblock", + "icon": "$(run)" + }, { "command": "vscode-deephaven.runSelection", "title": "Run Deephaven Selected Lines", @@ -677,6 +682,10 @@ "command": "vscode-deephaven.runCode", "when": "editorLangId == python || editorLangId == groovy" }, + { + "command": "vscode-deephaven.runMarkdownCodeblock", + "when": "false" + }, { "command": "vscode-deephaven.runSelection", "when": "editorLangId == python || editorLangId == groovy" diff --git a/src/common/commands.ts b/src/common/commands.ts index 64a483f9..1a3df7df 100644 --- a/src/common/commands.ts +++ b/src/common/commands.ts @@ -31,6 +31,7 @@ export const REFRESH_SERVER_CONNECTION_TREE_CMD = cmd( ); export const REFRESH_VARIABLE_PANELS_CMD = cmd('refreshVariablePanels'); export const RUN_CODE_COMMAND = cmd('runCode'); +export const RUN_MARKDOWN_CODEBLOCK_CMD = cmd('runMarkdownCodeBlock'); export const RUN_SELECTION_COMMAND = cmd('runSelection'); export const SEARCH_CONNECTIONS_CMD = cmd('searchConnections'); export const SEARCH_PANELS_CMD = cmd('searchPanels'); diff --git a/src/controllers/ConnectionController.ts b/src/controllers/ConnectionController.ts index 4582b337..b6cd35b5 100644 --- a/src/controllers/ConnectionController.ts +++ b/src/controllers/ConnectionController.ts @@ -14,7 +14,6 @@ import { createConnectionQuickPickOptions, createConnectStatusBarItem, getConsoleType, - getEditorForUri, isSupportedLanguageId, Logger, updateConnectionStatusBarItem, @@ -135,7 +134,11 @@ export class ConnectionController extends ControllerBase implements Disposable { const editor = vscode.window.activeTextEditor; - if (editor == null || !isSupportedLanguageId(editor.document.languageId)) { + if ( + editor == null || + (!isSupportedLanguageId(editor.document.languageId) && + editor.document.languageId !== 'markdown') + ) { this._connectStatusBarItem.hide(); return; } @@ -158,7 +161,8 @@ export class ConnectionController extends ControllerBase implements Disposable { connectEditor = async ( connectionOrServer: ConnectionState | ServerState, - editor: vscode.TextEditor + uri: vscode.Uri, + languageId: string ): Promise => { updateConnectionStatusBarItem(this._connectStatusBarItem, 'connecting'); @@ -167,7 +171,7 @@ export class ConnectionController extends ControllerBase implements Disposable { if ('url' in connectionOrServer) { const cn = await this._serverManager.connectToServer( connectionOrServer.url, - getConsoleType(editor.document.languageId) + getConsoleType(languageId) ); if (cn == null) { @@ -179,7 +183,11 @@ export class ConnectionController extends ControllerBase implements Disposable { } try { - await this._serverManager.setEditorConnection(editor, connectionOrServer); + await this._serverManager.setEditorConnection( + uri, + languageId, + connectionOrServer + ); } catch (err) { updateConnectionStatusBarItem(this._connectStatusBarItem, 'disconnected'); @@ -202,19 +210,19 @@ export class ConnectionController extends ControllerBase implements Disposable { /** * Get or create a connection for the given uri. - * @param uri + * @param uri Uri to get or create a connection for. + * @param languageId Language id to use for the connection. */ getOrCreateConnection = async ( - uri: vscode.Uri + uri: vscode.Uri, + languageId: string ): Promise => { assertDefined(this._outputChannel, 'outputChannel'); assertDefined(this._serverManager, 'serverManager'); assertDefined(this._toaster, 'toaster'); - const editor = await getEditorForUri(uri); - // Get existing connection for the editor - let dhService = await this._serverManager.getEditorConnection(editor); + let dhService = await this._serverManager.getEditorConnection(uri); if (dhService != null) { return dhService; @@ -222,7 +230,7 @@ export class ConnectionController extends ControllerBase implements Disposable { const supportingConnections = await getConnectionsForConsoleType( this._serverManager.getConnections(), - editor.document.languageId as ConsoleType + languageId as ConsoleType ); const availableServers = this._serverManager.getServers({ @@ -233,7 +241,7 @@ export class ConnectionController extends ControllerBase implements Disposable { if (supportingConnections.length === 1 && availableServers.length === 0) { // If we only have 1 supporting connection, and no available servers, use // the available connection. - await this.connectEditor(supportingConnections[0], editor); + await this.connectEditor(supportingConnections[0], uri, languageId); } else if ( // If there are no active connections that can support the editor, and we // only have 1 available server, just connect to it instead of prompting the @@ -241,10 +249,10 @@ export class ConnectionController extends ControllerBase implements Disposable { supportingConnections.length === 0 && availableServers.length === 1 ) { - await this.connectEditor(availableServers[0], editor); + await this.connectEditor(availableServers[0], uri, languageId); } else { // If there are multiple options to select, prompt the user to select one. - const isSelected = await this.onPromptUserToSelectConnection(); + const isSelected = await this.onPromptUserToSelectConnection(languageId); // User cancelled the selection or an error occurred if (!isSelected) { @@ -252,10 +260,10 @@ export class ConnectionController extends ControllerBase implements Disposable { } } - dhService = await this._serverManager.getEditorConnection(editor); + dhService = await this._serverManager.getEditorConnection(uri); if (dhService == null) { - const logMsg = `No active connection found supporting '${editor.document.languageId}' console type.`; + const logMsg = `No active connection found supporting '${languageId}' console type.`; logger.debug(logMsg); this._outputChannel.appendLine(logMsg); this._toaster.error(logMsg); @@ -341,12 +349,20 @@ export class ConnectionController extends ControllerBase implements Disposable { * 2. A list of running servers composed of: * - DHC servers that don't yet have a connection * - All running DHE servers + * @param languageId Optional language id to use for the connection. Defaults + * to the language id of the active editor. */ - onPromptUserToSelectConnection = async (): Promise => { + onPromptUserToSelectConnection = async ( + languageId?: string + ): Promise => { assertDefined(vscode.window.activeTextEditor, 'activeTextEditor'); assertDefined(this._serverManager, 'serverManager'); const editor = vscode.window.activeTextEditor; + const uri = editor.document.uri; + if (languageId == null) { + languageId = editor.document.languageId; + } const updateStatusPromise = this._serverManager.updateStatus(); @@ -382,12 +398,11 @@ export class ConnectionController extends ControllerBase implements Disposable { const connectionsForConsoleType: ConnectionState[] = await getConnectionsForConsoleType( this._serverManager.getConnections(), - editor.document.languageId as ConsoleType + languageId as ConsoleType ); - const editorActiveConnectionUrl = this._serverManager.getUriConnection( - editor.document.uri - )?.serverUrl; + const editorActiveConnectionUrl = + this._serverManager.getUriConnection(uri)?.serverUrl; let selectedCnResult: ConnectionState | ServerState | null = null; @@ -396,7 +411,7 @@ export class ConnectionController extends ControllerBase implements Disposable { createConnectionQuickPickOptions( [...runningDHCServersWithoutConnections, ...runningDHEServers], connectionsForConsoleType, - editor.document.languageId, + languageId, editorActiveConnectionUrl ) ); @@ -405,7 +420,7 @@ export class ConnectionController extends ControllerBase implements Disposable { return false; } - await this.connectEditor(selectedCnResult, editor); + await this.connectEditor(selectedCnResult, uri, languageId); return true; } catch (err) { diff --git a/src/controllers/ExtensionController.ts b/src/controllers/ExtensionController.ts index 9ee3d2f0..dced9bbc 100644 --- a/src/controllers/ExtensionController.ts +++ b/src/controllers/ExtensionController.ts @@ -10,6 +10,7 @@ import { REFRESH_SERVER_CONNECTION_TREE_CMD, REFRESH_SERVER_TREE_CMD, RUN_CODE_COMMAND, + RUN_MARKDOWN_CODEBLOCK_CMD, RUN_SELECTION_COMMAND, SEARCH_CONNECTIONS_CMD, SEARCH_PANELS_CMD, @@ -34,6 +35,7 @@ import { ServerConnectionTreeProvider, ServerConnectionPanelTreeProvider, runSelectedLinesHoverProvider, + RunMarkdownCodeBlockCodeLensProvider, } from '../providers'; import { DheJsApiCache, @@ -160,11 +162,16 @@ export class ExtensionController implements Disposable { */ initializeCodeLenses = (): void => { const codelensProvider = new RunCommandCodeLensProvider(); + const markdownCodelensProvider = new RunMarkdownCodeBlockCodeLensProvider(); this._context.subscriptions.push( codelensProvider, vscode.languages.registerCodeLensProvider('groovy', codelensProvider), - vscode.languages.registerCodeLensProvider('python', codelensProvider) + vscode.languages.registerCodeLensProvider('python', codelensProvider), + vscode.languages.registerCodeLensProvider( + 'markdown', + markdownCodelensProvider + ) ); }; @@ -468,6 +475,12 @@ export class ExtensionController implements Disposable { /** Run all code in active editor */ this.registerCommand(RUN_CODE_COMMAND, this.onRunCode); + /** Run Markdown codeblock */ + this.registerCommand( + RUN_MARKDOWN_CODEBLOCK_CMD, + this.onRunMarkdownCodeblock + ); + /** Run selected code in active editor */ this.registerCommand(RUN_SELECTION_COMMAND, this.onRunSelectedCode); @@ -626,7 +639,11 @@ export class ExtensionController implements Disposable { const editor = await vscode.window.showTextDocument(doc); - this._serverManager?.setEditorConnection(editor, dhService); + this._serverManager?.setEditorConnection( + editor.document.uri, + editor.document.languageId, + dhService + ); }; /** @@ -676,16 +693,37 @@ export class ExtensionController implements Disposable { await this._serverManager?.updateStatus(); }; + /** + * Run code block in markdown. + * @param uri The uri of the editor + * @param languageId The languageId of the code block + * @param range The range of the code block + */ + onRunMarkdownCodeblock = async ( + uri: vscode.Uri, + languageId: string, + range: vscode.Range + ): Promise => { + this.onRunCode(uri, undefined, [range], languageId); + }; + /** * Run all code in editor for given uri. - * @param uri - * @param arg - * @param selectionOnly + * @param uri The uri of the editor + * @param _arg Unused 2nd argument + * @param constrainTo Optional arg to constrain the code to run. + * - If 'selection', run only selected code in the editor. + * - If `undefined`, run all code in the editor. + * - If an array of vscode.Range, run only the code in the ranges (Note that + * partial lines will be expanded to include the full line content). + * @param languageId Optional languageId to run the code as. If none provided, + * use the languageId of the editor. */ onRunCode = async ( uri?: vscode.Uri, _arg?: { groupId: number }, - selectionOnly?: boolean + constrainTo?: 'selection' | vscode.Range[], + languageId?: string ): Promise => { assertDefined(this._connectionController, 'connectionController'); @@ -696,11 +734,18 @@ export class ExtensionController implements Disposable { assertDefined(uri, 'uri'); const editor = await getEditorForUri(uri); + if (languageId == null) { + languageId = editor.document.languageId; + } + const connectionState = - await this._connectionController.getOrCreateConnection(uri); + await this._connectionController.getOrCreateConnection(uri, languageId); if (isInstanceOf(connectionState, DhcService)) { - await connectionState?.runEditorCode(editor, selectionOnly === true); + const ranges: readonly vscode.Range[] | undefined = + constrainTo === 'selection' ? editor.selections : constrainTo; + + await connectionState?.runCode(editor.document, languageId, ranges); } }; @@ -713,7 +758,7 @@ export class ExtensionController implements Disposable { uri?: vscode.Uri, arg?: { groupId: number } ): Promise => { - this.onRunCode(uri, arg, true); + this.onRunCode(uri, arg, 'selection'); }; /** diff --git a/src/controllers/ServerConnectionTreeDragAndDropController.ts b/src/controllers/ServerConnectionTreeDragAndDropController.ts index ff1d1a52..9ed51890 100644 --- a/src/controllers/ServerConnectionTreeDragAndDropController.ts +++ b/src/controllers/ServerConnectionTreeDragAndDropController.ts @@ -35,7 +35,11 @@ export class ServerConnectionTreeDragAndDropController const editor = await getEditorForUri(uri); try { - await this.serverManager.setEditorConnection(editor, target); + await this.serverManager.setEditorConnection( + editor.document.uri, + editor.document.languageId, + target + ); } catch {} }; } diff --git a/src/providers/RunCommandCodeLensProvider.ts b/src/providers/RunCommandCodeLensProvider.ts index ae648925..0ea06089 100644 --- a/src/providers/RunCommandCodeLensProvider.ts +++ b/src/providers/RunCommandCodeLensProvider.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { ICON_ID } from '../common'; +import { ICON_ID, RUN_CODE_COMMAND } from '../common'; import type { Disposable } from '../types'; /** @@ -35,18 +35,11 @@ export class RunCommandCodeLensProvider const codeLenses: vscode.CodeLens[] = [ new vscode.CodeLens(new vscode.Range(0, 0, 0, 0), { title: `$(${ICON_ID.runAll}) Run Deephaven File`, - command: 'vscode-deephaven.runCode', + command: RUN_CODE_COMMAND, arguments: [document.uri], }), ]; return codeLenses; } - - resolveCodeLens?( - codeLens: vscode.CodeLens, - _token: vscode.CancellationToken - ): vscode.ProviderResult { - return codeLens; - } } diff --git a/src/providers/RunMarkdownCodeBlockCodeLensProvider.ts b/src/providers/RunMarkdownCodeBlockCodeLensProvider.ts new file mode 100644 index 00000000..28d5b291 --- /dev/null +++ b/src/providers/RunMarkdownCodeBlockCodeLensProvider.ts @@ -0,0 +1,72 @@ +import * as vscode from 'vscode'; +import { ICON_ID, RUN_MARKDOWN_CODEBLOCK_CMD } from '../common'; +import type { Disposable } from '../types'; + +/** + * Provides inline editor code lenses for running Deephaven codeblocks in + * Markdown files. + */ +export class RunMarkdownCodeBlockCodeLensProvider + implements vscode.CodeLensProvider, Disposable +{ + constructor() { + vscode.workspace.onDidChangeConfiguration(() => { + this._onDidChangeCodeLenses.fire(); + }); + vscode.window.onDidChangeTextEditorSelection(() => { + this._onDidChangeCodeLenses.fire(); + }); + } + + private readonly _onDidChangeCodeLenses: vscode.EventEmitter = + new vscode.EventEmitter(); + + readonly onDidChangeCodeLenses: vscode.Event = + this._onDidChangeCodeLenses.event; + + dispose = async (): Promise => { + this._onDidChangeCodeLenses.dispose(); + }; + + provideCodeLenses( + document: vscode.TextDocument, + _token: vscode.CancellationToken + ): vscode.ProviderResult { + const lines = document.getText().split('\n'); + + const ranges: [string, vscode.Range][] = []; + let start: vscode.Position | null = null; + let languageId = ''; + + // Create ranges for each code block in the document + for (let i = 0; i < lines.length; ++i) { + const line = lines[i]; + + if (line === '```' && start) { + ranges.push([ + languageId, + new vscode.Range(start, new vscode.Position(i - 1, 0)), + ]); + start = null; + } else if (line === '```python' || line === '```groovy') { + languageId = line.substring(3); + start = new vscode.Position(i + 1, 0); + } + } + + const codeLenses: vscode.CodeLens[] = ranges.map( + ([languageId, range]) => + new vscode.CodeLens( + // Put the code lens on the line before the code block + new vscode.Range(range.start.line - 1, 0, range.start.line - 1, 0), + { + title: `$(${ICON_ID.runSelection}) Run Deephaven Block`, + command: RUN_MARKDOWN_CODEBLOCK_CMD, + arguments: [document.uri, languageId, range], + } + ) + ); + + return codeLenses; + } +} diff --git a/src/providers/RunSelectedLinesHoverProvider.ts b/src/providers/RunSelectedLinesHoverProvider.ts index 41886466..374f39a0 100644 --- a/src/providers/RunSelectedLinesHoverProvider.ts +++ b/src/providers/RunSelectedLinesHoverProvider.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { ICON_ID, RUN_SELECTION_COMMAND } from '../common'; -import { expandSelectionToFullLines } from '../util'; +import { expandRangeToFullLines } from '../util'; /** * Provides hover content for running selected lines in Deephaven. @@ -15,7 +15,7 @@ export const runSelectedLinesHoverProvider: vscode.HoverProvider = { // Determine if hover is over a selected line const isOverSelection = editor.selections.some(selection => - expandSelectionToFullLines(editor.document)(selection).contains(position) + expandRangeToFullLines(editor.document)(selection).contains(position) ); if (!isOverSelection) { diff --git a/src/providers/index.ts b/src/providers/index.ts index 48c9fe8a..42c00aa7 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,4 +1,5 @@ export * from './RunCommandCodeLensProvider'; +export * from './RunMarkdownCodeBlockCodeLensProvider'; export * from './RunSelectedLinesHoverProvider'; export * from './ServerConnectionTreeProvider'; export * from './ServerTreeProvider'; diff --git a/src/services/DhcService.ts b/src/services/DhcService.ts index 484f98e7..da790832 100644 --- a/src/services/DhcService.ts +++ b/src/services/DhcService.ts @@ -4,7 +4,7 @@ import type { dh as DhcType } from '@deephaven/jsapi-types'; import { assertDefined, formatTimestamp, - getCombinedSelectedLinesText, + getCombinedRangeLinesText, Logger, saveRequirementsTxt, } from '../util'; @@ -324,12 +324,13 @@ export class DhcService implements IDhcService { await saveRequirementsTxt(dependencies); } - async runEditorCode( - editor: vscode.TextEditor, - selectionOnly = false + async runCode( + document: vscode.TextDocument, + languageId: string, + ranges?: readonly vscode.Range[] ): Promise { // Clear previous diagnostics when cmd starts running - this.diagnosticsCollection.set(editor.document.uri, []); + this.diagnosticsCollection.set(document.uri, []); if (this.session == null) { await this.initSession(); @@ -341,16 +342,14 @@ export class DhcService implements IDhcService { const [consoleType] = await this.cn.getConsoleTypes(); - if (consoleType !== editor.document.languageId) { - this.toaster.error( - `This connection does not support '${editor.document.languageId}'.` - ); + if (consoleType !== languageId) { + this.toaster.error(`This connection does not support '${languageId}'.`); return; } - const text = selectionOnly - ? getCombinedSelectedLinesText(editor) - : editor.document.getText(); + const text = ranges + ? getCombinedRangeLinesText(document, ranges) + : document.getText(); logger.info('Sending text to dh:', text); @@ -382,15 +381,14 @@ export class DhcService implements IDhcService { this.outputChannel.appendLine(error); this.toaster.error('An error occurred when running a command'); - if (editor.document.languageId === 'python') { + if (languageId === 'python') { const { line, value } = parseServerError(error); if (line != null) { - // If selectionOnly is true, the line number in the error will be - // relative to the selection (Python line numbers are 1 based. vscode - // line numbers are zero based.) - const fileLine = - (selectionOnly ? line + editor.selection.start.line : line) - 1; + // If ranges were provided, the line number in the error will be + // relative to the ranges content (Python line numbers are 1 based. + // vscode line numbers are zero based.) + const fileLine = (ranges ? line + ranges[0].start.line : line) - 1; // There seems to be an error for certain Python versions where line // numbers are shown as -1. In such cases, we'll just mark the first @@ -399,7 +397,7 @@ export class DhcService implements IDhcService { // Zero length will flag a token instead of a line const lineLength = - fileLine < 0 ? 0 : editor.document.lineAt(fileLine).text.length; + fileLine < 0 ? 0 : document.lineAt(fileLine).text.length; // Diagnostic representing the line of code that produced the server error const diagnostic: vscode.Diagnostic = { @@ -409,7 +407,7 @@ export class DhcService implements IDhcService { source: 'deephaven', }; - this.diagnosticsCollection.set(editor.document.uri, [diagnostic]); + this.diagnosticsCollection.set(document.uri, [diagnostic]); } } diff --git a/src/services/ServerManager.ts b/src/services/ServerManager.ts index 18ea0417..1d4d8609 100644 --- a/src/services/ServerManager.ts +++ b/src/services/ServerManager.ts @@ -471,13 +471,12 @@ export class ServerManager implements IServerManager { }; /** - * Get the connection associated with the URI of the given editor. - * @param editor + * Get the connection associated with the given URI. + * @param uri */ getEditorConnection = async ( - editor: vscode.TextEditor + uri: vscode.Uri ): Promise => { - const uri = editor.document.uri; return this._uriConnectionsMap.get(uri) ?? null; }; @@ -532,20 +531,18 @@ export class ServerManager implements IServerManager { }; setEditorConnection = async ( - editor: vscode.TextEditor, + uri: vscode.Uri, + languageId: string, connectionState: ConnectionState ): Promise => { - const uri = editor.document.uri; - const isConsoleTypeSupported = - isInstanceOf(connectionState, DhcService) && - (await connectionState.supportsConsoleType( - editor.document.languageId as ConsoleType - )); + languageId === 'markdown' || + (isInstanceOf(connectionState, DhcService) && + (await connectionState.supportsConsoleType(languageId as ConsoleType))); if (!isConsoleTypeSupported) { throw new UnsupportedConsoleTypeError( - `Connection '${connectionState.serverUrl}' does not support '${editor.document.languageId}'.` + `Connection '${connectionState.serverUrl}' does not support '${languageId}'.` ); } diff --git a/src/types/serviceTypes.d.ts b/src/types/serviceTypes.d.ts index cbfcc456..a0d8010f 100644 --- a/src/types/serviceTypes.d.ts +++ b/src/types/serviceTypes.d.ts @@ -20,6 +20,7 @@ import type { CoreUnauthenticatedClient, Psk, CoreAuthenticatedClient, + NonEmptyArray, } from '../types/commonTypes'; import type { AuthenticatedClient as DheAuthenticatedClient, @@ -55,9 +56,10 @@ export interface IDhcService extends Disposable, ConnectionState { getConsoleTypes: () => Promise>; supportsConsoleType: (consoleType: ConsoleType) => Promise; - runEditorCode: ( - editor: vscode.TextEditor, - selectionOnly?: boolean + runCode: ( + document: vscode.TextDocument, + languageId: string, + ranges?: readonly vscode.Range[] ) => Promise; } @@ -155,15 +157,14 @@ export interface IServerManager extends Disposable { getConnection: (serverUrl: URL) => ConnectionState | undefined; getConnections: () => ConnectionState[]; getConnectionUris: (connection: ConnectionState) => vscode.Uri[]; - getEditorConnection: ( - editor: vscode.TextEditor - ) => Promise; + getEditorConnection: (uri: vscode.Uri) => Promise; getWorkerCredentials: ( serverOrWorkerUrl: URL | WorkerURL ) => Promise; getWorkerInfo: (workerUrl: WorkerURL) => Promise; setEditorConnection: ( - editor: vscode.TextEditor, + uri: vscode.Uri, + languageId: string, dhService: ConnectionState ) => Promise; diff --git a/src/util/selectionUtils.ts b/src/util/selectionUtils.ts index 8b68abce..3913b626 100644 --- a/src/util/selectionUtils.ts +++ b/src/util/selectionUtils.ts @@ -1,50 +1,52 @@ import * as vscode from 'vscode'; /** - * Combine content of all selected lines (including multi-cursor selections). - * Any line that is partially selected will be included in its entirety. - * @param editor + * Combine content of all range lines. Any partial lines will be expanded to + * include the full line content. + * @param document The document to extract the range lines from. + * @param ranges The ranges to extract the lines from. */ -export function getCombinedSelectedLinesText( - editor: vscode.TextEditor +export function getCombinedRangeLinesText( + document: vscode.TextDocument, + ranges: readonly vscode.Range[] ): string { - return sortSelections(editor.selections) - .map(expandSelectionToFullLines(editor.document)) - .map(selection => editor.document.getText(selection)) + return sortRanges(ranges) + .map(expandRangeToFullLines(document)) + .map(range => document.getText(range)) .join('\n'); } /** - * Create a function that can expand selection to include the full lines of any - * lines that are included in the selection. - * @param document - * @returns + * Create a function that can expand a range to include the full lines of any + * lines that are included in the range. + * @param document The document to provide line lengths for a given range. + * @returns Function that expands a range to include full lines. */ -export function expandSelectionToFullLines(document: vscode.TextDocument) { +export function expandRangeToFullLines(document: vscode.TextDocument) { /** - * Expand a given selection to include the full lines of any lines that are included - * in the selection. + * Expand a given range to include the full lines of any lines that are + * included in the range. * @param selection */ - return (selection: vscode.Selection): vscode.Selection => { - return new vscode.Selection( - selection.start.line, + return (range: vscode.Range): vscode.Range => { + return new vscode.Range( + range.start.line, 0, - selection.end.line, - document.lineAt(selection.end.line).text.length + range.end.line, + document.lineAt(range.end.line).text.length ); }; } /** - * Sort selections in document order. Useful for converting `TextEditor.selections` + * Sort ranges in document order. Useful for converting `TextEditor.selections` * which are ordered based on multi-cursor creation order. - * @param selections + * @param ranges The ranges to sort. */ -export function sortSelections( - selections: readonly vscode.Selection[] -): vscode.Selection[] { - return [...selections].sort( +export function sortRanges( + ranges: readonly TRange[] +): TRange[] { + return [...ranges].sort( (s1, s2) => s1.start.line - s2.start.line || s1.start.character - s2.start.character );