Created
March 17, 2023 22:35
-
-
Save jcreedcmu/a40910558e070231cf2a6c10a50bbcd2 to your computer and use it in GitHub Desktop.
Snippet of typescript that modifies itself in an emacs buffer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*** | |
* 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