August 5, 2022
By: Kevin
org-mode中增加csharp的执行能力
扩展emacs
Emacs的扩展浩瀚如海, 基本能涵盖我们所需, 总有好心人先我们一步遇到并解决了问题.
但也不总是这样, 总有些足够特别(聪明)的需求, 需要自我满足一下.
对于非主流语言(是的, 在windows之外的世界, c#是个非主流语言), 扩展是不全的. C#有csharp-mode, 可以做自动格式化和语法高亮.
对于emacs29以后, csharp-mode已经内置, 但是缺乏对c#语言一些最新特性的支持(比如说多行字符串$@"")
但是org-mode中的代码执行(babel扩展)是不全的.
想在emacs里写笔记, 所以, 体验想和写python,lisp,shell一样, 能用org-babel执行我的C#代码.
这涉及到另一个问题: 一段c#代码执行, 需要有工程文件, main函数, 编译后再执行, 这个有点太烦琐了.
解决方案
- dotnet可以执行后缀为csx的脚本, 接近交互式的使用c#, 需要安装一下:
dotnet tool install -g dotnet-script安装成功后可以执行下dotnet script进入一个可以执行c#的shell. - 自己写个babel的c#扩展
保存为一个独立文件, 放到emacs的可访问目录
(require 'ob) (require 'csharp-mode) (add-to-list 'org-babel-tangle-lang-exts '("csharp" . "cs")) (defvar org-babel-default-header-args:csharp '()) (defun ob-csharp--build-script-run-command (cmdline path) "Create run command according to the PATH." (format "dotnet script %s %s" path (or cmdline " "))) (defun org-babel-execute:csharp (body params) (let* ((processed-params (org-babel-process-params params)) (cmpflag (or (cdr (assoc :cmpflag params)) "")) (cmdline (or (cdr (assoc :cmdline params)) "")) (src-temp (org-babel-temp-file "csharp-src-" ".csx"))) (with-temp-file src-temp (insert body)) (let ((results (org-babel-eval (ob-csharp--build-script-run-command cmdline src-temp) ""))) (org-babel-reassemble-table (org-babel-result-cond (cdr (assoc :result-params params)) (org-babel-read results) (let ((tmp-file (org-babel-temp-file "c-"))) (with-temp-file tmp-file (insert results)) (org-babel-import-elisp-from-file tmp-file))) (org-babel-pick-name (cdr (assoc :colname-names params)) (cdr (assoc :colnames params))) (org-babel-pick-name (cdr (assoc :rowname-names params)) (cdr (assoc :rownames params))))))) (provide 'ob-csharp) ;;; ob-csharp.el ends here - 在org-babel-load-languages中增加csharp
(org-babel-do-load-languages
'org-babel-load-languages
'((R . t)
(csharp . t) ;; <= 增加在此
(python . t)
(js . t)
(shell . t)
(emacs-lisp . t)
(lisp . t)
(gnuplot . t)))
结果&展望
c#的执行结果
#+begin_src csharp :results pp
Console.WriteLine("hello world!");
#+end_src
#+RESULTS:
: hello world!
我们可以借助这个方法在org-mode中执行任意想执行的语言, 比如java(提示:java11 引入了一个叫jshell的东西).
#+begin_src java :results pp
System.out.println("hello world!");
#+end_src
#+RESULTS:
: hello world!
使用release模式执行c#代码
因为我需要对c#的某些函数做性能测试, 必须在release模式下执行代码, 这样使用dotnet-script的这个模式就不是合适了, 所以我对这个插件进行了扩展.
使用如下配置, 注意增加了:release yes启动release 模式执行, 可以使用 :nuget '("lib1" "lib2") 动态从nuget下载使用的库.
#+begin_src csharp :release yes :nuget '("BenchmarkDotNet")
// 要测试的program.cs内容
#+end_src
新的ob-csharp.el的代码如下
(require 'ob)
(require 'csharp-mode)
(add-to-list 'org-babel-tangle-lang-exts '("csharp" . "cs"))
(defvar org-babel-default-header-args:csharp
'((:release . "no")
(:nuget . nil))
"Default arguments for C# Babel blocks.")
(defun ob-csharp--build-script-run-command (cmdline path)
"Create run command according to the PATH."
(format "dotnet script %s %s" (or cmdline "") path))
(defun ob-csharp--create-temp-project (nuget-deps code)
"Create a temporary C# project with NUGET-DEPS and CODE.
Returns the path to the project directory."
(let ((temp-dir (make-temp-file "ob-csharp-project-" t)))
(let ((default-directory temp-dir))
;; Initialize new console project
(unless (zerop (call-process "dotnet" nil nil nil "new" "console"))
(error "Failed to create new dotnet console project"))
;; Add NuGet packages if any
(when (and nuget-deps (listp nuget-deps))
(dolist (dep nuget-deps)
(let ((args (split-string dep)))
(unless (zerop (apply 'call-process "dotnet" nil nil nil "add" "package" args))
(error "Failed to add NuGet package: %s" dep)))))
;; Overwrite Program.cs with user code
(let ((program-cs (expand-file-name "Program.cs" temp-dir)))
(with-temp-file program-cs
(insert code)))
;; Verify that the .csproj file exists
(let ((csproj-files (directory-files temp-dir nil "\\.csproj$")))
(unless csproj-files
(error "Project file (.csproj) not found in %s" temp-dir)))
temp-dir)))
(defun org-babel-execute:csharp (body params)
"Execute a C# code block with BODY and PARAMS."
(let* ((processed-params (org-babel-process-params params))
(release (string= (or (cdr (assoc :release processed-params)) "no") "yes"))
(nuget (cdr (assoc :nuget processed-params)))
(cmdline (or (cdr (assoc :cmdline processed-params)) ""))
(result "")
(output-buffer "*ob-csharp-output*"))
(if release
;; Release mode: create project, build, and run
(let ((project-dir (ob-csharp--create-temp-project nuget body)))
(unwind-protect
(let ((default-directory project-dir))
;; Build the project in Release mode
(unless (zerop (call-process "dotnet" nil output-buffer t "build" "-c" "Release"))
(error "Failed to build the C# project. See %s for details." output-buffer))
;; Run the project
;; 清空输出缓冲区
(with-current-buffer (get-buffer-create output-buffer)
(erase-buffer))
(unless (zerop (call-process "dotnet" nil output-buffer t "run" "-c" "Release"))
(error "Failed to run the C# project. See %s for details." output-buffer))
;; Capture the output
(with-current-buffer output-buffer
(setq result (buffer-string))))
;; Clean up: optionally delete the temp project directory
(delete-directory project-dir t)))
;; Script mode: existing behavior
(let ((src-temp (org-babel-temp-file "csharp-src-" ".csx")))
(with-temp-file src-temp (insert body))
(setq result (org-babel-eval (ob-csharp--build-script-run-command cmdline src-temp) ""))))
;; Return the result, trimming any trailing whitespace
(string-trim result)))
(provide 'ob-csharp)
;;; ob-csharp.el ends here