Skip to content

Instantly share code, notes, and snippets.

@jcreedcmu
Created March 17, 2023 22:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcreedcmu/a40910558e070231cf2a6c10a50bbcd2 to your computer and use it in GitHub Desktop.
Save jcreedcmu/a40910558e070231cf2a6c10a50bbcd2 to your computer and use it in GitHub Desktop.
Snippet of typescript that modifies itself in an emacs buffer
/***
* A little bit of research into what would be needed for a "direct manipulation" toy in emacs.
* - NodeJS allows replacing `Error.prepareStackStrace` with a custom handler that can obtain
* first-class stack trace data.
* - An arbitrary function call (see below, `knob(1429)` for example) can obtain a stack
* trace and inspect it to find out where in the file the actual call to knob(1429) took place.
* - Since we're in typescript, we can also find the sourcemap and use it to map this into
* a location into the original `.ts` file.
* - We can shell out to emacsclient to evaluate arbitrary elisp
* - It can in turn go to the appropriate place in the active buffer viewing the `.ts` file,
* and modify the argument to `knob(...)` however it likes.
*
* The imagined application for this rube goldberg device is that UI
* controls could backfeed information into a typescript source file
* that constructed UI controls in the first place.
***/
import * as sourceMap from 'source-map';
import * as fs from 'fs';
import * as path from 'path';
import * as cp from 'child_process';
function prepareStackTrace(e: Error, frames: NodeJS.CallSite[]): NodeJS.CallSite[] {
return frames;
}
type Info = { file: string, line: number, column: number };
function getCaller(): Info {
const prevPst = Error.prepareStackTrace;
Error.prepareStackTrace = prepareStackTrace;
const stack = (new Error().stack) as unknown as NodeJS.CallSite[];
const info: Info[] = stack.map(cs => {
return { file: cs.getFileName() ?? '', line: cs.getLineNumber() ?? -1, column: cs.getColumnNumber() ?? -1 };
});
const rv = info[2];
Error.prepareStackTrace = prevPst;
return rv;
}
type Knob = {
source: Info,
name: string,
val: number,
};
let counter = 0;
function knob(val: number): Knob {
counter++;
return { source: getCaller(), name: `c${counter}`, val }
}
function withSetText(body: string): string {
return `
(flet ((set-text (file line col new-val)
(set-buffer (find-file-noselect file))
(save-excursion
(goto-char (point-min))
(forward-line (- line 1))
(move-to-column col)
(let* ((_ (re-search-forward "("))
(b (point))
(_ (re-search-forward ")"))
(e (point)))
(goto-char b)
(delete-char (- e b 1))
(insert new-val))))) ${body})
`;
}
async function go() {
const smc = await new sourceMap.SourceMapConsumer(fs.readFileSync(__filename + '.map', 'utf8'));
function replace(knob: Knob, wth: number): void {
const orig = smc.originalPositionFor(knob.source);
if (orig.line == null || orig.column == null) throw new Error('null line or column');
const origFile = path.resolve(path.dirname(knob.source.file), orig.source ?? '');
const output = cp.execFileSync('emacsclient', ['-e', withSetText(`(set-text "${origFile}" ${orig.line} ${orig.column} "${wth}")`)]);
console.log(output.toString('utf8'));
}
try {
const k1 = knob(1429);
const k2 = knob(106);
replace(k1, k1.val + 1);
replace(k2, k2.val + 20);
} catch (e) {
console.log('Error:', e);
}
}
go();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment