Skip to content

Commit

Permalink
Merge pull request #291 from codefori/feature/extract_to_procedure
Browse files Browse the repository at this point in the history
Extract Procedure
  • Loading branch information
worksofliam authored Jan 22, 2024
2 parents 2b14484 + e4cc1d1 commit 5f4eec2
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 8 deletions.
9 changes: 7 additions & 2 deletions extension/server/src/providers/linter/codeActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CodeAction, CodeActionParams, Range } from 'vscode-languageserver';
import { getActions, refreshLinterDiagnostics } from '.';
import { CodeAction, CodeActionKind, CodeActionParams, Range } from 'vscode-languageserver';
import { getActions, getExtractProcedureAction, refreshLinterDiagnostics } from '.';
import { documents, parser } from '..';

export default async function codeActionsProvider(params: CodeActionParams): Promise<CodeAction[]|undefined> {
Expand All @@ -13,6 +13,11 @@ export default async function codeActionsProvider(params: CodeActionParams): Pro
const docs = await parser.getDocs(document.uri);

if (docs) {
const extractOption = getExtractProcedureAction(document, docs, range);
if (extractOption) {
return [extractOption];
}

const detail = await refreshLinterDiagnostics(document, docs, false);
if (detail) {
const fixErrors = detail.errors.filter(error =>
Expand Down
80 changes: 80 additions & 0 deletions extension/server/src/providers/linter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,84 @@ export function getActions(document: TextDocument, errors: IssueRange[]) {
});

return actions;
}

export function getExtractProcedureAction(document: TextDocument, docs: Cache, range: Range): CodeAction|undefined {
if (range.end.line > range.start.line) {
const linesRange = Range.create(range.start.line, 0, range.end.line, 1000);
const references = docs.referencesInRange({position: document.offsetAt(linesRange.start), end: document.offsetAt(linesRange.end)});
const validRefs = references.filter(ref => [`struct`, `subitem`, `variable`].includes(ref.dec.type));

if (validRefs.length > 0) {
const lastLine = document.offsetAt({line: document.lineCount, character: 0});

const nameDiffSize = 1; // Always once since we only add 'p' at the start
const newParamNames = validRefs.map(ref => `p${ref.dec.name}`);
let procedureBody = document.getText(linesRange);

const rangeStartOffset = document.offsetAt(linesRange.start);

// Fix the found offset lengths to be relative to the new procedure
for (let i = validRefs.length - 1; i >= 0; i--) {
for (let y = validRefs[i].refs.length - 1; y >= 0; y--) {
validRefs[i].refs[y] = {
position: validRefs[i].refs[y].position - rangeStartOffset,
end: validRefs[i].refs[y].end - rangeStartOffset
};
}
}

// Then let's fix the references to use the new names
for (let i = validRefs.length - 1; i >= 0; i--) {
for (let y = validRefs[i].refs.length - 1; y >= 0; y--) {
const ref = validRefs[i].refs[y];

procedureBody = procedureBody.slice(0, ref.position) + newParamNames[i] + procedureBody.slice(ref.end);
ref.end += nameDiffSize;

// Then we need to update the offset of the next references
for (let z = i - 1; z >= 0; z--) {
for (let x = validRefs[z].refs.length - 1; x >= 0; x--) {
if (validRefs[z].refs[x].position > ref.end) {
validRefs[z].refs[x] = {
position: validRefs[z].refs[x].position + nameDiffSize,
end: validRefs[z].refs[x].end + nameDiffSize
};
}
}
}
}
}

const newProcedure = [
`Dcl-Proc NewProcedure;`,
` Dcl-Pi *N;`,
...validRefs.map((ref, i) => ` ${newParamNames[i]} ${ref.dec.type === `struct` ? `LikeDS` : `Like`}(${ref.dec.name});`),
` End-Pi;`,
``,
procedureBody,
`End-Proc;`
].join(`\n`)

const newAction = CodeAction.create(`Extract to new procedure`, CodeActionKind.RefactorExtract);

// First do the exit
newAction.edit = {
changes: {
[document.uri]: [
TextEdit.replace(linesRange, `NewProcedure(${validRefs.map(r => r.dec.name).join(`:`)});`),
TextEdit.insert(document.positionAt(lastLine), `\n\n`+newProcedure)
]
},
};

// Then format the document
newAction.command = {
command: `editor.action.formatDocument`,
title: `Format`
};

return newAction;
}
}
}
31 changes: 25 additions & 6 deletions language/models/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { indicators1 } from "../../tests/suite";
import { CacheProps, IncludeStatement, Keywords } from "../parserTypes";
import { CacheProps, IncludeStatement, Keywords, Offset } from "../parserTypes";
import Declaration from "./declaration";

const newInds = () => {
Expand Down Expand Up @@ -199,26 +199,45 @@ export default class Cache {
}
}

static referenceByOffset(scope: Cache, offset: number): Declaration|undefined {
referencesInRange(range: Offset): { dec: Declaration, refs: Offset[] }[] {
let list: { dec: Declaration, refs: Offset[] }[] = [];

for (let i = range.position; i <= range.end; i++) {
const ref = Cache.referenceByOffset(this, i);
if (ref) {
// No duplicates allowed
if (list.some(item => item.dec.name === ref.name)) continue;

list.push({
dec: ref,
refs: ref.references.filter(r => r.offset.position >= range.position && r.offset.end <= range.end).map(r => r.offset)
})
};
}

return list;
}

static referenceByOffset(scope: Cache, offset: number): Declaration | undefined {
const props: (keyof Cache)[] = [`parameters`, `subroutines`, `procedures`, `files`, `variables`, `structs`, `constants`, `indicators`];

for (const prop of props) {
const list = scope[prop] as unknown as Declaration[];
for (const def of list) {
let possibleRef: boolean;

// Search top level
possibleRef = def.references.some(r => offset >= r.offset.position && offset <= r.offset.end);
if (possibleRef) return def;

// Search any subitems
if (def.subItems.length > 0) {
for (const subItem of def.subItems) {
possibleRef = subItem.references.some(r => offset >= r.offset.position && offset <= r.offset.end);
if (possibleRef) return subItem;
}
}

// Search scope if any
if (def.scope) {
const inScope = Cache.referenceByOffset(def.scope, offset);
Expand Down
37 changes: 37 additions & 0 deletions tests/suite/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3463,4 +3463,41 @@ exports.on_excp_2 = async () => {
expectedIndent: 2,
currentIndent: 0
});
}

exports.range_1 = async () => {
const lines = [
`**free`,
`ctl-opt debug option(*nodebugio: *srcstmt) dftactgrp(*no) actgrp(*caller)`,
`main(Main);`,
`dcl-s x timestamp;`,
`dcl-s y timestamp;`,
`dcl-proc Main;`,
` dsply %CHAR(CalcDiscount(10000));`,
` dsply %char(CalcDiscount(1000));`,
` x = %TIMESTAMP(y);`,
` y = %TimeStamp(x);`,
` return;`,
`end-proc;`,
].join(`\n`);

const cache = await parser.getDocs(uri, lines, {ignoreCache: true, withIncludes: true});
Linter.getErrors({ uri, content: lines }, {
CollectReferences: true
}, cache);

const rangeRefs = cache.referencesInRange({position: 220, end: 260});
assert.strictEqual(rangeRefs.length, 2);
assert.ok(rangeRefs[0].dec.name === `x`);
assert.ok(rangeRefs[1].dec.name === `y`);

assert.deepStrictEqual(rangeRefs[0].refs, [
{ position: 220, end: 221 },
{ position: 256, end: 257 }
]);

assert.deepStrictEqual(rangeRefs[1].refs, [
{ position: 235, end: 236 },
{ position: 241, end: 242 }
]);
}

0 comments on commit 5f4eec2

Please sign in to comment.