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

Diff viewer for sliced code #70

Merged
merged 5 commits into from
Mar 14, 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
3 changes: 2 additions & 1 deletion example/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"vscode-flowr.verboseLog": true
"vscode-flowr.verboseLog": true,
"vscode-flowr.style.sliceDisplay": "diff"
}
28 changes: 28 additions & 0 deletions example/example-large.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
library(cranlogs)

# retrieve top N packages
N <- 500

# Get the names of all packages
pkgnames <- available.packages(repos="https://cloud.r-project.org")[,1]

# init result
download_numbers <- data.frame()

stepsize <- 750
for (i in seq(1, length(pkgnames), by = stepsize)) {
start <- i
end <- min(i+stepsize-1, length(pkgnames))
cat("Retrieving download numbers for packages", start, "to", end, "from total", length(pkgnames), "\n")
data <- cran_downloads(packages = pkgnames[start:end], when="last-month")
download_numbers <- rbind(download_numbers, data)
}

# now packages appear often as they are with different days in the data, therefore we aggregate
download_numbers <- aggregate(count ~ package, data=download_numbers, sum)

# sort download numbers
download_numbers <- download_numbers[order(-download_numbers$count),]
print(download_numbers[1:N,])

write.table(download_numbers[1:N,], file="top-r-downloads.txt", quote=FALSE, sep=",", row.names=FALSE, col.names=FALSE)
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@
"maximum": 1,
"markdownDescription": "The opacity that code which isn't part of the current slice should have, between 0 (invisible) and 1 (opaque)."
},
"vscode-flowr.style.sliceDisplay": {
"type": "string",
"default": "text",
"enum": [
"text",
"diff"
],
"enumDescriptions": [
"Code that isn't part of the current slice will be grayed out in the active editor.",
"Open a diff view that shows the current slice and the rest of the file."
],
"markdownDescription": "The way that slices should be displayed."
},
"vscode-flowr.verboseLog": {
"type": "boolean",
"default": false,
Expand Down
64 changes: 4 additions & 60 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,23 @@
import * as vscode from 'vscode'
import { FlowrInternalSession } from './flowr/internal-session'
import { FlowrServerSession } from './flowr/server-session'
import type { NodeId } from '@eagleoutice/flowr'
import type { SourceRange } from '@eagleoutice/flowr/util/range'
import { Settings } from './settings'
import { registerSliceCommands } from './slice'

export const MINIMUM_R_MAJOR = 3
export const BEST_R_MAJOR = 4

export let flowrSession: FlowrInternalSession | FlowrServerSession | undefined
export let outputChannel: vscode.OutputChannel
export let sliceDecoration: vscode.TextEditorDecorationType

let flowrStatus: vscode.StatusBarItem

export function activate(context: vscode.ExtensionContext) {
console.log('Loading vscode-flowr')

outputChannel = vscode.window.createOutputChannel('flowR')
recreateSliceDecorationType()

context.subscriptions.push(vscode.commands.registerCommand('vscode-flowr.slice.cursor', async() => {
const activeEditor = vscode.window.activeTextEditor
if(activeEditor?.selection) {
if(!flowrSession) {
await establishInternalSession()
}
void flowrSession?.retrieveSlice(activeEditor.selection.active, activeEditor, true)
}
}))
context.subscriptions.push(vscode.commands.registerCommand('vscode-flowr.slice.clear', () => {
const activeEditor = vscode.window.activeTextEditor
if(activeEditor) {
activeEditor.setDecorations(sliceDecoration, [])
}
}))
context.subscriptions.push(vscode.commands.registerCommand('vscode-flowr.slice.cursor-reconstruct', async() => {
const activeEditor = vscode.window.activeTextEditor
if(activeEditor) {
if(!flowrSession) {
await establishInternalSession()
}
const code = await flowrSession?.retrieveSlice(activeEditor.selection.active, activeEditor, false)
const doc = await vscode.workspace.openTextDocument({language: 'r', content: code})
void vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside)
}
}))

registerSliceCommands(context)

context.subscriptions.push(vscode.commands.registerCommand('vscode-flowr.session.connect', () => {
establishServerSession()
Expand All @@ -64,16 +36,7 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(flowrStatus)
updateStatusBar()

vscode.workspace.onDidChangeConfiguration(e => {
if(e.affectsConfiguration(`${Settings.Category}.${Settings.StyleSliceOpacity}`)) {
recreateSliceDecorationType()
}
})

context.subscriptions.push(new vscode.Disposable(() => {
sliceDecoration.dispose()
destroySession()
}))
context.subscriptions.push(new vscode.Disposable(() => destroySession()))
process.on('SIGINT', () => destroySession())

if(getConfig().get<boolean>(Settings.ServerAutoConnect)) {
Expand Down Expand Up @@ -121,22 +84,3 @@ export function updateStatusBar() {
flowrStatus.hide()
}
}

export function createSliceDecorations(document: vscode.TextDocument, sliceElements: { id: NodeId, location: SourceRange }[]): vscode.DecorationOptions[]{
// create a set to make finding matching lines
const sliceLines = new Set<number>(sliceElements.map(s => s.location.start.line - 1))
const ret: vscode.DecorationOptions[] = []
for(let i = 0; i < document.lineCount; i++) {
if(!sliceLines.has(i)) {
ret.push({range: new vscode.Range(i, 0, i, document.lineAt(i).text.length)})
}
}
return ret
}

function recreateSliceDecorationType() {
sliceDecoration?.dispose()
sliceDecoration = vscode.window.createTextEditorDecorationType({
opacity: getConfig().get<number>(Settings.StyleSliceOpacity)?.toString()
})
}
13 changes: 7 additions & 6 deletions src/flowr/internal-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import type { NodeId, RShellOptions, SingleSlicingCriterion} from '@eagleoutice/
import { LAST_STEP, requestFromInput, RShell, SteppingSlicer } from '@eagleoutice/flowr'
import type { SourceRange } from '@eagleoutice/flowr/util/range'
import { isNotUndefined } from '@eagleoutice/flowr/util/assert'
import { BEST_R_MAJOR, MINIMUM_R_MAJOR, createSliceDecorations, getConfig, isVerbose, sliceDecoration, updateStatusBar } from '../extension'
import { BEST_R_MAJOR, MINIMUM_R_MAJOR, getConfig, isVerbose, updateStatusBar } from '../extension'
import { Settings } from '../settings'
import { displaySlice } from '../slice'

export class FlowrInternalSession {

Expand Down Expand Up @@ -75,12 +76,12 @@ export class FlowrInternalSession {
this.shell?.close()
}

async retrieveSlice(pos: vscode.Position, editor: vscode.TextEditor, decorate: boolean): Promise<string> {
async retrieveSlice(pos: vscode.Position, editor: vscode.TextEditor, display: boolean): Promise<string> {
if(!this.shell) {
return ''
}
try {
return await this.extractSlice(this.shell, editor, pos, decorate)
return await this.extractSlice(this.shell, editor, pos, display)
} catch(e) {
this.outputChannel.appendLine('Error: ' + (e as Error)?.message);
(e as Error).stack?.split('\n').forEach(l => this.outputChannel.appendLine(l))
Expand All @@ -89,7 +90,7 @@ export class FlowrInternalSession {
}
}

private async extractSlice(shell: RShell, editor: vscode.TextEditor, pos: vscode.Position, decorate: boolean): Promise<string> {
private async extractSlice(shell: RShell, editor: vscode.TextEditor, pos: vscode.Position, display: boolean): Promise<string> {
const filename = editor.document.fileName
const content = FlowrInternalSession.fixEncoding(editor.document.getText())

Expand Down Expand Up @@ -119,8 +120,8 @@ export class FlowrInternalSession {
return a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
})

if(decorate) {
editor.setDecorations(sliceDecoration, createSliceDecorations(editor.document, sliceElements))
if(display) {
void displaySlice(editor, sliceElements)
}
if(isVerbose()) {
this.outputChannel.appendLine('slice: ' + JSON.stringify([...result.slice.result]))
Expand Down
9 changes: 5 additions & 4 deletions src/flowr/server-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { visitAst } from '@eagleoutice/flowr'
import type { SourceRange } from '@eagleoutice/flowr/util/range'
import { isNotUndefined } from '@eagleoutice/flowr/util/assert'
import { FlowrInternalSession } from './internal-session'
import { createSliceDecorations, establishInternalSession, getConfig, isVerbose, sliceDecoration, updateStatusBar } from '../extension'
import { establishInternalSession, getConfig, isVerbose, updateStatusBar } from '../extension'
import type { FlowrHelloResponseMessage } from '@eagleoutice/flowr/cli/repl/server/messages/hello'
import { Settings } from '../settings'
import { displaySlice } from '../slice'

export class FlowrServerSession {

Expand Down Expand Up @@ -115,7 +116,7 @@ export class FlowrServerSession {
})
}

async retrieveSlice(pos: vscode.Position, editor: vscode.TextEditor, decorate: boolean): Promise<string> {
async retrieveSlice(pos: vscode.Position, editor: vscode.TextEditor, display: boolean): Promise<string> {
const filename = editor.document.fileName
const content = FlowrInternalSession.fixEncoding(editor.document.getText())

Expand Down Expand Up @@ -152,8 +153,8 @@ export class FlowrServerSession {
return a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
})

if(decorate) {
editor.setDecorations(sliceDecoration, createSliceDecorations(editor.document, sliceElements))
if(display) {
void displaySlice(editor, sliceElements)
}
if(isVerbose()) {
this.outputChannel.appendLine('slice: ' + JSON.stringify([...sliceResponse.results.slice.result]))
Expand Down
3 changes: 3 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export enum Settings {
ServerAutoConnect = 'server.autoConnect',

StyleSliceOpacity = 'style.sliceOpacity',
StyleSliceDisplay = 'style.sliceDisplay',

Rexecutable = 'r.executable',
}

export type SliceDisplay = 'text' | 'diff'
79 changes: 79 additions & 0 deletions src/slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as vscode from 'vscode'
import type { NodeId } from '@eagleoutice/flowr'
import type { SourceRange } from '@eagleoutice/flowr/util/range'
import { establishInternalSession, flowrSession, getConfig } from './extension'
import type { SliceDisplay } from './settings'
import { Settings } from './settings'

export let sliceDecoration: vscode.TextEditorDecorationType

export function registerSliceCommands(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.commands.registerCommand('vscode-flowr.slice.cursor', async() => {
const activeEditor = vscode.window.activeTextEditor
if(activeEditor?.selection) {
if(!flowrSession) {
await establishInternalSession()
}
void flowrSession?.retrieveSlice(activeEditor.selection.active, activeEditor, true)
}
}))
context.subscriptions.push(vscode.commands.registerCommand('vscode-flowr.slice.clear', () => {
const activeEditor = vscode.window.activeTextEditor
if(activeEditor) {
activeEditor.setDecorations(sliceDecoration, [])
}
}))
context.subscriptions.push(vscode.commands.registerCommand('vscode-flowr.slice.cursor-reconstruct', async() => {
const activeEditor = vscode.window.activeTextEditor
if(activeEditor) {
if(!flowrSession) {
await establishInternalSession()
}
const code = await flowrSession?.retrieveSlice(activeEditor.selection.active, activeEditor, false)
const doc = await vscode.workspace.openTextDocument({language: 'r', content: code})
void vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside)
}
}))

recreateSliceDecorationType()
vscode.workspace.onDidChangeConfiguration(e => {
if(e.affectsConfiguration(`${Settings.Category}.${Settings.StyleSliceOpacity}`)) {
recreateSliceDecorationType()
}
})
context.subscriptions.push(new vscode.Disposable(() => sliceDecoration.dispose()))
}

export async function displaySlice(editor: vscode.TextEditor, sliceElements: { id: NodeId, location: SourceRange }[]) {
const sliceLines = new Set<number>(sliceElements.map(s => s.location.start.line - 1))
switch(getConfig().get<SliceDisplay>(Settings.StyleSliceDisplay)) {
case 'text': {
const decorations: vscode.DecorationOptions[] = []
for(let i = 0; i < editor.document.lineCount; i++) {
if(!sliceLines.has(i)) {
decorations.push({range: new vscode.Range(i, 0, i, editor.document.lineAt(i).text.length)})
}
}
editor.setDecorations(sliceDecoration, decorations)
break
}
case 'diff': {
const sliceContent = []
for(let i = 0; i < editor.document.lineCount; i++){
if(!sliceLines.has(i)){
sliceContent.push(editor.document.lineAt(i).text)
}
}
const sliceDoc = await vscode.workspace.openTextDocument({language: 'r', content: sliceContent.join('\n')})
void vscode.commands.executeCommand('vscode.diff', sliceDoc.uri, editor.document.uri)
break
}
}
}

function recreateSliceDecorationType() {
sliceDecoration?.dispose()
sliceDecoration = vscode.window.createTextEditorDecorationType({
opacity: getConfig().get<number>(Settings.StyleSliceOpacity)?.toString()
})
}
Loading