【依存ゼロ】管理者権限のない企業LinuxでVimをSAS専用IDEにする(導入編)
この記事について
社内のLinuxサーバでSAS 9.3を使っているが、管理者権限がなく ~/ 配下しか
編集できない——そんな環境でも、Vimだけで SAS開発を完結させるための設定一式
「SASIDE」です。
- 外部依存ゼロ: Node.js / Python / Lua / LSP / Tree-sitter / ネットワーク不要
-
root不要:
~/.vimにファイルを置くだけ - git / curl / apt 不要: 1本のシェルスクリプトを実行するだけ
-
sas program.txt運用に対応: 拡張子が.txtでも中身からSASと自動判定
会社PCがGitHubにアクセスできなくても、この記事のコードをコピペするだけで
導入できるよう、インストーラを1本のテキストにまとめてあります。
関連記事:
- 使い方編(実際のワークフロー)
- リファレンス編(全コマンド・全オプション)
できること
| カテゴリ | 機能 |
|---|---|
| 実行 |
F5 で 保存→SAS実行→ログ表示→ERROR/WARNINGをquickfixへ |
| 部分実行 | ビジュアル選択して F5 で選択範囲だけ、\rb でカーソル位置のステップだけ実行 |
| 構文チェック |
\rc で obs=0 実行。巨大データでも数秒でタイプミス検出 |
| ログ解析 | 色分け表示、ERROR/WARNING/NOTE抽出、危険NOTE検出(\lr) |
| 構文 | SAS 9.3対応ハイライト(%macro・PROC SQL・マクロ変数・日付定数) |
| インデント | DATA/PROC/DO/%MACRO のネストと括弧内を賢く字下げ、F9で一括整形 |
| 補完 | SAS関数/PROC/オプション辞書 + マクロ変数 + データセット名を Tab で補完 |
| ナビゲート | Explorer(F2) / Navigator(F3) / Outline(F4) / データセット・マクロ一覧 |
| 入力支援 | 括弧・クオート自動補完、スニペット(Ctrl+j)、テンプレート生成 |
| その他 | プロジェクト検索、TODO一覧、ブックマーク、セッション保存 |
特に推したい2機能
危険NOTE検出(\lr) — SASで本当に怖いのはERRORではなく、
NOTE: Variable X is uninitialized や
NOTE: MERGE statement has more than one data set with repeats of BY values のように
エラーにならず黙って誤った結果を出すNOTEです。これらのパターンを辞書化し、
「なぜ危険か」の理由付きでquickfixに一覧化します。
構文チェック(\rc) — -obs 0 -noreplace を付けて実行し、データを1件も
処理せずに構文だけ検証します。数百万件を読むプログラムでも、タイプミスの発覚が
「10分後」から「数秒後」になります。
動作要件
- Linux + Vim 8.0以降(Huge版推奨)
-
+jobがあればSASを非同期実行、無ければ自動で同期実行にフォールバック - 管理者権限は不要
Vim 7.4系でも大半は動きますが、
Included patches:が 2200 未満だと
一部機能(ログ→quickfix連携、スニペット展開)が動きません。
vim --version | head -2で確認してください。
導入方法
Step 1. この記事末尾のインストーラ全文をコピー
「インストーラ全文」のコードブロック右上のコピーボタンで、
4000行超でもワンクリックで全文コピーできます。
Step 2. 会社PCでテキストファイルとして保存
メモ帳などに貼り付け、saside_install.txt として保存します。
(.sh だとメール等でブロックされやすいので、まず .txt が無難)
Step 3. WinSCPでサーバへ転送
転送モードを「テキスト」にすると改行が自動でLinux形式に変換されます。
Step 4. サーバ上で実行
# バイナリ転送だった場合の保険(改行コードをLinux形式へ)
tr -d '\r' < saside_install.txt > saside_install.sh
# 実行(sudoは不要。sudoで実行しないでください)
sh saside_install.sh
~/.vim に約50ファイルが展開されます。既存ファイルは ~/.vim_backup_日時/ へ
自動退避されるので、何度実行しても安全です。
Step 5. vimrc を反映
# ~/.vimrc がまだ無い場合
cp ~/.vim/vimrc.example ~/.vimrc
# 既にある場合は中身を確認して追記(重複行に注意)
cat ~/.vim/vimrc.example >> ~/.vimrc
Step 6. ヘルプの索引を生成(初回のみ)
vim -c 'helptags ~/.vim/doc' -c 'q'
以後 :help saside が引けます。
Step 7. 動作確認
:edit test.sas
:set filetype? " → filetype=sas
:SasBlockInfo " ブロック情報が出ればOK
SASの実体がPATHに無い場合は ~/.vimrc の末尾で指定します。
let g:saside_sas_cmd = '/opt/sas/SASFoundation/9.3/sas'
よくあるトラブル
インストール後、Vim起動時にエラーが出る / 一部ファイルの中身が途中から始まっている
コピペや転送の途中で行が欠落した場合に起きます。このインストーラは展開後に
全ファイルの行数・バイト数を自動照合し、欠落があれば NG: ファイル名 と
表示して停止します。その表示が出たら、記事のコードブロックを(コピーボタンで)
もう一度コピーし直してやり直してください。巨大なブロックはメモ帳や
クリップボード経由で稀に行が欠落するため、整合性チェック: 全 N ファイル OK が
出るまで再実行するのが確実です。
:syntax on で E484 エラーが出る
この種の環境は $VIMRUNTIME が壊れている(~/.vim を指している)ことが多く、
~/.vim/syntax/syntax.vim などの配線ファイルが必須です。インストーラに同梱済み
なので、展開できていれば解消します。
ペーストするとVimが固まる
括弧補完などが1文字ごとに発動するためです。貼り付け前に F11 を押して
貼り付けモードにすると一瞬で貼れます(貼り終えたら再度 F11)。
そもそもファイルはWinSCPで転送する想定なので、Vimへの直接ペーストは不要です。
Permission denied で止まる
~/.vim 内にroot所有のファイルが残っている場合に起きます。
chown -R $USER:$USER ~/.vim で自分の所有に戻してから再実行してください。
ログが文字化けする
~/.vimrc の set fileencodings=... の順序を環境に合わせます。
EUC-JP環境なら euc-jp を前方に。
アンインストール / 元に戻す
~/.vim_backup_日時/ に元のファイルが入っています。丸ごと戻したい場合は
そのディレクトリの内容を ~/.vim に上書きコピーしてください。
ライセンス・注意事項
MIT License。自由に改変・再配布できます。
社内利用者の方へ: このスクリプト自体に社内情報は含まれませんが、
~/.vimrc や辞書ファイルに社内のサーバパス・ライブラリ名・データセット名を
追記した場合は、公開前に必ず除去してください。また、外部で作成したスクリプトを
社内サーバで実行することについては、事前に情報システム部門や上長へ確認することを
おすすめします(このインストーラは全文が人間に読めるプレーンテキストで、外部通信も
一切ないため、内容レビューによる承認を得やすい形にしてあります)。
インストーラ全文
以下を丸ごとコピーし、saside_install.txt として保存してください。
#!/bin/sh
# saside_install.sh — SAS Vim環境 セルフエクストラクト・インストーラ(自己検証付き)
# 使い方: sh saside_install.sh (~/.vim に展開。既存ファイルは自動退避)
# 注意: sudoは不要です($HOME/.vim にしか書き込みません)
#
# ★このインストーラは展開後に各ファイルの行数・バイト数を照合します。
# コピペや転送で内容が欠落すると「NG」として一覧表示されるので、
# その場合は記事のコードを再コピーしてやり直してください。
set -e
TARGET="$HOME/.vim"
BACKUP="$HOME/.vim_backup_$(date +%Y%m%d_%H%M%S)"
count=0
write_file() {
path="$TARGET/$1"; dir=$(dirname "$path"); mkdir -p "$dir"
if [ -f "$path" ]; then mkdir -p "$BACKUP/$(dirname "$1")"; cp -p "$path" "$BACKUP/$1"; fi
cat > "$path"; count=$((count + 1))
}
echo "SAS Vim環境をインストールします: $TARGET"
write_file 'autoload/sas.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/autoload/sas.vim
" sas#xxx() が初めて呼ばれた時にのみ読み込まれる(遅延ロード)。
function! sas#run(path) abort
let logf = fnamemodify(a:path, ':r') . '.log'
let lstf = fnamemodify(a:path, ':r') . '.lst'
let cmd = printf('%s -sysin %s -log %s -print %s -nosplash',
\ g:sas_command, shellescape(a:path), shellescape(logf), shellescape(lstf))
echom "Running: " . cmd
call system(cmd)
call sas#quickfixlog(logf)
endfunction
function! sas#openlog(path) abort
if filereadable(a:path)
execute 'split ' . fnameescape(a:path)
setlocal filetype=saslog
else
echohl WarningMsg | echo "log not found: " . a:path | echohl None
endif
endfunction
function! sas#openlst(path) abort
if filereadable(a:path)
execute 'split ' . fnameescape(a:path)
setlocal filetype=saslst
else
echohl WarningMsg | echo "lst not found: " . a:path | echohl None
endif
endfunction
function! sas#quickfixlog(path) abort
if !filereadable(a:path)
echohl WarningMsg | echo "log not found: " . a:path | echohl None
return
endif
let save_efm = &errorformat
setlocal errorformat=ERROR\ %n-%.%#:\ %m,ERROR:\ %m,WARNING:\ %m
execute 'cgetfile ' . fnameescape(a:path)
let &errorformat = save_efm
copen
endfunction
" :SasFormat / <F9> 用。IntelliJの「コードの整形」に相当する
" 安全な範囲の自動整形(完全なAST解析はしていない点に注意)。
" 1) gg=G でブロック構造に基づく再インデント(indent/sas.vim を使用)
" 2) 各行の行末の余分な空白を除去
" 3) 3行以上連続する空行を1行に圧縮
function! sas#format() abort
let view = winsaveview()
normal! gg=G
keepjumps keeppatterns %s/\s\+$//e
keepjumps keeppatterns %s/\n\{3,}/\r\r/e
call winrestview(view)
echo "SasFormat: 整形しました"
endfunction
" スマート % : SASのブロック対応(DO/END, %DO/%END, %MACRO/%MEND,
" DATA・PROC/RUN・QUIT)の間をジャンプする。どのブロックキーワード上でも
" なければ通常の % (括弧ジャンプ)として振る舞う
function! sas#matchjump() abort
let line = getline('.')
let pat_do = '\c\<do\>\%(\s\+until\|\s\+while\)\?.\{-};\s*$'
let pat_end = '\c^\s*end\s*;'
let pat_pdo = '\c%do\>'
let pat_pend = '\c^\s*%end\s*;'
let pat_macro = '\c^\s*%macro\>'
let pat_mend = '\c^\s*%mend\>'
let pat_dp = '\c^\s*\%(data\|proc\)\>'
let pat_rq = '\c^\s*\%(run\|quit\)\s*;'
if line =~ pat_macro
call search('\c^\s*%mend\>', 'W')
elseif line =~ pat_mend
call searchpair(pat_macro, '', pat_mend, 'bW')
elseif line =~ pat_pend
call searchpair(pat_pdo, '', pat_pend, 'bW')
elseif line =~ pat_pdo
" カーソルを %do キーワード上に移してから対応する %end を探す
" (キーワードより手前にカーソルがあると searchpair が対応を誤るため)
call search('\c%do\>', 'c', line('.'))
call searchpair(pat_pdo, '', pat_pend, 'W')
elseif line =~ pat_end
call searchpair(pat_do, '', pat_end, 'bW')
elseif line =~ pat_do
" カーソルを do キーワード上に移してから対応する end を探す
call search('\c\<do\>', 'c', line('.'))
call searchpair(pat_do, '', pat_end, 'W')
elseif line =~ pat_dp
call search(pat_rq, 'W')
elseif line =~ pat_rq
call search(pat_dp, 'bW')
else
normal! %
endif
endfunction
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/bookmark.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/bookmark.vim
" Module: bookmark — 名前付きブックマーク(JSON永続化)
" Description: {file, lnum, text, name} を ~/.vim/.state/bookmarks.json に
" 保存する。一覧は quickfix に流す。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
let s:marks = []
let s:loaded = 0
function! s:file() abort
return saside#util#StateDir() . '/bookmarks.json'
endfunction
function! s:load() abort
if s:loaded
return
endif
let s:loaded = 1
if filereadable(s:file())
try
let s:marks = json_decode(join(readfile(s:file()), ''))
catch
call saside#util#Warn('bookmarks.json が壊れています(初期化)')
let s:marks = []
endtry
endif
endfunction
function! s:save() abort
call writefile([json_encode(s:marks)], s:file())
endfunction
" 現在行をブックマークに追加する
function! saside#bookmark#Add(...) abort
call s:load()
let name = a:0 && a:1 !=# '' ? a:1 : input('ブックマーク名: ')
call add(s:marks, {
\ 'file': expand('%:p'),
\ 'lnum': line('.'),
\ 'text': getline('.'),
\ 'name': name,
\ })
call s:save()
call saside#event#Emit('BookmarkChanged', {})
call saside#util#Info('追加: ' . name . ' (' . len(s:marks) . '件)')
endfunction
" ブックマーク一覧を quickfix に表示する
function! saside#bookmark#List() abort
call s:load()
if empty(s:marks)
call saside#util#Info('ブックマークはありません')
return
endif
let items = map(copy(s:marks), {_, m -> {
\ 'filename': m.file, 'lnum': m.lnum,
\ 'text': '[' . m.name . '] ' . m.text}})
call setqflist([], ' ', {'title': 'SASIDE bookmarks', 'items': items})
botright cwindow
endfunction
" 全ブックマークを削除する
function! saside#bookmark#Clear() abort
call s:load()
let s:marks = []
call s:save()
call saside#event#Emit('BookmarkChanged', {})
call saside#util#Info('全ブックマークを削除しました')
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/complete.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/complete.vim
" Module: complete — SAS補完エンジン(omnifunc)
" Description: 候補源は4系統:
" 1) dict/sas_*.dict(関数/PROC/ODS/FORMAT/オプション/マクロ。
" "語<TAB>説明" 形式。初回のみロードしキャッシュ)
" 2) parser インデックス(マクロ変数・ユーザーマクロ・
" データセット名)
" 3) parser#Keywords()(ステートメント)
" 4) バッファ既出語
" '&'開始→マクロ変数、'%'開始→マクロ系に自動で切り替わる。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
let s:dicts = {} " category -> [{word, desc}]
let s:loaded = 0
let s:DICT_FILES = {
\ 'func': 'sas_functions.dict',
\ 'proc': 'sas_procs.dict',
\ 'ods': 'sas_ods.dict',
\ 'fmt': 'sas_formats.dict',
\ 'opt': 'sas_options.dict',
\ 'macro': 'sas_macros.dict',
\ }
" 辞書をロードする(runtimepath の dict/ から。初回のみ)
function! s:load_dicts() abort
if s:loaded
return
endif
let s:loaded = 1
for [cat, fname] in items(s:DICT_FILES)
let s:dicts[cat] = []
let found = globpath(&runtimepath, 'dict/' . fname, 0, 1)
if empty(found)
continue
endif
for line in readfile(found[0])
if line =~# '^\s*#' || line =~# '^\s*$'
continue
endif
let parts = split(line, "\t")
call add(s:dicts[cat],
\ {'word': parts[0], 'desc': get(parts, 1, '')})
endfor
endfor
endfunction
" 語の説明を辞書から引く(help モジュールが利用)。無ければ ''
function! saside#complete#Describe(word) abort
call s:load_dicts()
let w = tolower(a:word)
for cat in keys(s:dicts)
for e in s:dicts[cat]
if tolower(e.word) ==# w || tolower(e.word) ==# '%' . w
return '[' . cat . '] ' . e.word
\ . (e.desc !=# '' ? ' — ' . e.desc : '')
endif
endfor
endfor
return ''
endfunction
" omnifunc 本体
function! saside#complete#Omni(findstart, base) abort
if a:findstart
let line = getline('.')
let start = col('.') - 1
while start > 0 && line[start - 1] =~ '[[:alnum:]_&%]'
let start -= 1
endwhile
return start
endif
call s:load_dicts()
let idx = saside#parser#Index()
let base = a:base
let items = []
" ---- &xxx : マクロ変数 ----
if base =~# '^&'
let prefix = strpart(base, 1)
for v in idx.macrovars
if prefix ==# '' || v =~? '^' . escape(prefix, '\')
call add(items, {'word': '&' . v . '.', 'abbr': '&' . v,
\ 'menu': '[mvar]'})
endif
endfor
return items
endif
" ---- %xxx : マクロ構文・ユーザーマクロ ----
if base =~# '^%'
for e in s:dicts.macro
if e.word =~? '^' . escape(base, '\')
call add(items, {'word': e.word, 'menu': '[macro]', 'info': e.desc})
endif
endfor
for m in idx.macros
let cand = '%' . m.name
if cand =~? '^' . escape(base, '\')
call add(items, {'word': cand,
\ 'menu': '[user] (' . join(m.args, ',') . ')'})
endif
endfor
return items
endif
" ---- 通常語 ----
let pat = '^' . escape(base, '\')
let seen = {}
for kw in saside#parser#Keywords().statement
if kw =~? pat
call add(items, {'word': kw, 'menu': '[stmt]'})
let seen[kw] = 1
endif
endfor
for [cat, entries] in items(s:dicts)
if cat ==# 'macro'
continue
endif
for e in entries
if e.word =~? pat && !has_key(seen, tolower(e.word))
call add(items, {'word': e.word, 'menu': '[' . cat . ']',
\ 'info': e.desc})
let seen[tolower(e.word)] = 1
endif
endfor
endfor
" データセット名(& を含むものは除外して実名のみ)
for d in idx.datasets
if d.name =~? pat && d.name !~# '&' && !has_key(seen, tolower(d.name))
call add(items, {'word': d.name, 'menu': '[ds]'})
let seen[tolower(d.name)] = 1
endif
endfor
" バッファ既出語
let cur = line('.')
for lnum in range(1, line('$'))
if lnum == cur
continue
endif
for w in split(getline(lnum), '\W\+')
if strlen(w) >= 3 && w =~# '^\a' && w =~? pat
\ && !has_key(seen, tolower(w))
call add(items, {'word': w, 'menu': '[buf]'})
let seen[tolower(w)] = 1
endif
endfor
endfor
return items
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/dataset.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/dataset.vim
" Module: dataset — Dataset Browser(navigator の表示モード委譲)
" =============================================================================
function! saside#dataset#Toggle() abort
call saside#navigator#Toggle('dataset')
endfunction
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/event.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/event.vim
" Module: event — SASIDE pub/sub イベント基盤
" Description: User autocmd を薄くラップしたイベント機構。
" 発火データは g:saside_event に格納する(Vim8のUser autocmdは
" 引数を渡せないための定石)。再帰発火は1段までに制限する。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
let s:emit_depth = 0
const s:MAX_DEPTH = 2
" イベントを発火する。
" name: 'IndexUpdated' など PascalCase
" data: 任意のdict。ハンドラは g:saside_event で参照する
function! saside#event#Emit(name, data) abort
if s:emit_depth >= s:MAX_DEPTH
call saside#util#Warn('event loop detected, dropped: ' . a:name)
return v:false
endif
let s:emit_depth += 1
" ネストしたEmit(ハンドラ内からの発火)が g:saside_event を上書きしても、
" 外側イベントの後続ハンドラが正しいデータを受け取れるよう退避・復元する
let had_prev = exists('g:saside_event')
let prev = had_prev ? g:saside_event : {}
try
let g:saside_event = extend({'name': a:name}, a:data)
if exists('#User#saside:' . a:name)
execute 'doautocmd <nomodeline> User saside:' . a:name
endif
finally
if had_prev
let g:saside_event = prev
else
unlet! g:saside_event
endif
let s:emit_depth -= 1
endtry
return v:true
endfunction
" イベントを購読する(after/plugin からのユーザー拡張用ヘルパ)。
" name: イベント名
" Fn: funcref または関数名文字列。g:saside_event を引数に呼ばれる
"
" 実装メモ: funcref(特にlambda)を文字列化してautocmdに埋め込むと、
" 参照が残らずGCされて E700 になる。script-local のレジストリに保持し、
" ディスパッチ関数経由で呼び出す。
let s:handlers = {}
let s:handler_seq = 0
function! saside#event#On(name, Fn) abort
let s:handler_seq += 1
let key = a:name . '#' . s:handler_seq
let s:handlers[key] = a:Fn
execute 'augroup saside_user_' . a:name
execute 'autocmd User saside:' . a:name
\ . ' call saside#event#_Dispatch(' . string(key) . ')'
execute 'augroup END'
return key
endfunction
" 内部ディスパッチャ(公開名だがユーザーが直接呼ぶものではない)
function! saside#event#_Dispatch(key) abort
if has_key(s:handlers, a:key)
call call(s:handlers[a:key], [g:saside_event])
endif
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/explorer.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/explorer.vim
" Module: explorer — Project Explorer(左ペイン・軽量ツリー)
" Description: readdir()(無ければglob())によるディレクトリツリー。
" ファイル/ディレクトリの作成・削除・コピー・リネーム・移動を
" Vim組込み関数のみで行う(外部コマンド不使用)。
" Keymaps(バッファ内):
" Enter/o 開く・ディレクトリ展開/折畳 a 新規ファイル A 新規ディレクトリ
" D 削除(確認あり) r リネーム m 移動 c コピー
" u ルートを親へ C ルートをカーソル下のディレクトリへ R 再読込 q 閉じる
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
let s:root = ''
let s:expanded = {}
" Explorerを開く(ルート未設定なら現在ファイルのディレクトリ)
function! saside#explorer#Open(...) abort
if a:0 && a:1 !=# ''
let s:root = fnamemodify(a:1, ':p')
elseif s:root ==# ''
let base = expand('%:p:h')
let s:root = fnamemodify(base !=# '' ? base : getcwd(), ':p')
endif
call saside#layout#Open('explorer')
let bufnr = saside#util#Scratch('explorer')
call s:render(bufnr)
call s:setup_buffer()
return v:true
endfunction
function! saside#explorer#Toggle() abort
if saside#layout#Win('explorer') != -1
call saside#layout#Close('explorer')
else
call saside#explorer#Open()
endif
endfunction
" -----------------------------------------------------------------------------
" 描画
" -----------------------------------------------------------------------------
" dir直下のエントリを [ディレクトリ...] + [ファイル...] で返す
function! s:list(dir) abort
if exists('*readdir')
let names = readdir(a:dir)
else
" glob フォールバック(ドットファイルは省略)
let names = map(glob(a:dir . '/*', 1, 1), "fnamemodify(v:val, ':t')")
endif
let dirs = []
let files = []
for n in sort(names)
if n =~# '^\.' | continue | endif
if isdirectory(a:dir . '/' . n)
call add(dirs, n)
else
call add(files, n)
endif
endfor
return [dirs, files]
endfunction
function! s:render(bufnr) abort
let lines = ['[' . fnamemodify(s:root, ':~') . ']']
let paths = ['']
call s:render_dir(s:root, 0, lines, paths)
call saside#util#SetLines(a:bufnr, lines)
call setbufvar(a:bufnr, 'saside_paths', paths)
endfunction
function! s:render_dir(dir, depth, lines, paths) abort
let [dirs, files] = s:list(a:dir)
let pad = repeat(' ', a:depth)
for d in dirs
let full = a:dir . '/' . d
let open = get(s:expanded, full, 0)
call add(a:lines, pad . (open ? '▾ ' : '▸ ') . d . '/')
call add(a:paths, full)
if open
call s:render_dir(full, a:depth + 1, a:lines, a:paths)
endif
endfor
for f in files
call add(a:lines, pad . ' ' . f)
call add(a:paths, a:dir . '/' . f)
endfor
endfunction
function! s:refresh() abort
let bufnr = bufnr('saside://explorer')
if bufnr != -1
call s:render(bufnr)
endif
endfunction
" カーソル行のパスを返す(ヘッダ行は '')
function! s:cursor_path() abort
return get(get(b:, 'saside_paths', []), line('.') - 1, '')
endfunction
" -----------------------------------------------------------------------------
" 操作
" -----------------------------------------------------------------------------
function! saside#explorer#Activate() abort
let path = s:cursor_path()
if path ==# '' | return | endif
if isdirectory(path)
let s:expanded[path] = !get(s:expanded, path, 0)
call s:refresh()
else
call win_gotoid(saside#layout#Editor())
execute 'edit ' . fnameescape(path)
endif
endfunction
function! saside#explorer#NewFile() abort
let dir = s:context_dir()
let name = input('新規ファイル名: ')
if name ==# '' | return | endif
let path = dir . '/' . name
if filereadable(path) || isdirectory(path)
call saside#util#Error('既に存在します: ' . name)
return
endif
call writefile([], path)
call s:refresh()
endfunction
function! saside#explorer#NewDir() abort
let dir = s:context_dir()
let name = input('新規ディレクトリ名: ')
if name ==# '' | return | endif
call mkdir(dir . '/' . name, 'p')
let s:expanded[dir . '/' . name] = 1
call s:refresh()
endfunction
function! saside#explorer#Delete() abort
let path = s:cursor_path()
if path ==# '' | return | endif
if confirm('削除しますか? ' . fnamemodify(path, ':t'),
\ "&Yes\n&No", 2) != 1
return
endif
let flag = isdirectory(path) ? 'rf' : ''
if delete(path, flag) != 0
call saside#util#Error('削除に失敗: ' . path)
endif
call s:refresh()
endfunction
function! saside#explorer#Rename() abort
let path = s:cursor_path()
if path ==# '' | return | endif
let name = input('リネーム: ', fnamemodify(path, ':t'))
if name ==# '' || name ==# fnamemodify(path, ':t') | return | endif
let dest = fnamemodify(path, ':h') . '/' . name
if rename(path, dest) != 0
call saside#util#Error('リネームに失敗')
endif
call s:refresh()
endfunction
function! saside#explorer#Move() abort
let path = s:cursor_path()
if path ==# '' | return | endif
let dest_dir = input('移動先ディレクトリ: ', fnamemodify(path, ':h'), 'dir')
if dest_dir ==# '' || !isdirectory(dest_dir)
call saside#util#Error('ディレクトリがありません')
return
endif
if rename(path, dest_dir . '/' . fnamemodify(path, ':t')) != 0
call saside#util#Error('移動に失敗')
endif
call s:refresh()
endfunction
function! saside#explorer#Copy() abort
let path = s:cursor_path()
if path ==# '' || isdirectory(path)
call saside#util#Warn('ファイルのみコピーできます')
return
endif
let name = input('コピー先ファイル名: ', fnamemodify(path, ':t'))
if name ==# '' | return | endif
let dest = fnamemodify(path, ':h') . '/' . name
if filereadable(dest)
call saside#util#Error('既に存在します')
return
endif
" バイナリ安全なコピー(外部コマンド不使用)
call writefile(readfile(path, 'b'), dest, 'b')
call s:refresh()
endfunction
function! saside#explorer#RootUp() abort
let s:root = fnamemodify(s:root, ':h:h') . '/'
call s:refresh()
endfunction
function! saside#explorer#RootHere() abort
let path = s:cursor_path()
if isdirectory(path)
let s:root = fnamemodify(path, ':p')
call s:refresh()
endif
endfunction
" コンテキストディレクトリ(カーソルがディレクトリならそこ、ファイルなら親)
function! s:context_dir() abort
let path = s:cursor_path()
if path ==# ''
return fnamemodify(s:root, ':p:h')
endif
return isdirectory(path) ? path : fnamemodify(path, ':h')
endfunction
function! s:setup_buffer() abort
if get(b:, 'saside_exp_ready', 0)
return
endif
let b:saside_exp_ready = 1
setlocal cursorline
nnoremap <buffer> <silent> <CR> :call saside#explorer#Activate()<CR>
nnoremap <buffer> <silent> o :call saside#explorer#Activate()<CR>
nnoremap <buffer> <silent> a :call saside#explorer#NewFile()<CR>
nnoremap <buffer> <silent> A :call saside#explorer#NewDir()<CR>
nnoremap <buffer> <silent> D :call saside#explorer#Delete()<CR>
nnoremap <buffer> <silent> r :call saside#explorer#Rename()<CR>
nnoremap <buffer> <silent> m :call saside#explorer#Move()<CR>
nnoremap <buffer> <silent> c :call saside#explorer#Copy()<CR>
nnoremap <buffer> <silent> u :call saside#explorer#RootUp()<CR>
nnoremap <buffer> <silent> C :call saside#explorer#RootHere()<CR>
nnoremap <buffer> <silent> R :call <SID>refresh_map()<CR>
nnoremap <buffer> <silent> q :call saside#layout#Close('explorer')<CR>
syntax match SasideExpDir /^.*[▸▾].*$/
highlight default link SasideExpDir Directory
endfunction
function! s:refresh_map() abort
call s:refresh()
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/format.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/format.vim
" Module: format — コード整形
" Description: 1) indent/sas.vim による全体再インデント
" 2) 行末空白の除去
" 3) 3行以上の連続空行を1行へ圧縮
" 4) g:saside_format_upcase=1 のとき予約語を大文字化
" (文字列/コメント内は syntax 情報で保護する)
" 全処理は1回のundoで戻せる。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
function! saside#format#Format() abort
let view = winsaveview()
" undo を1塊にする
let ul_save = &l:undolevels
try
normal! gg=G
keepjumps keeppatterns silent! %s/\s\+$//e
keepjumps keeppatterns silent! %s/\n\{3,}/\r\r/e
if get(g:, 'saside_format_upcase', 0)
call s:upcase_buffer()
endif
finally
let &l:undolevels = ul_save
call winrestview(view)
endtry
call saside#util#Info('整形しました')
endfunction
" 予約語(ステートメント語のみ)を大文字化する。
" 各マッチ位置の syntax グループを調べ、文字列/コメントはスキップする。
function! s:upcase_buffer() abort
let words = saside#parser#Keywords().statement
let pat = '\c\<\%(' . join(words, '\|') . '\)\>'
for lnum in range(1, line('$'))
let line = getline(lnum)
let out = ''
let pos = 0
while 1
let [str, mstart, mend] = matchstrpos(line, pat, pos)
if mstart == -1
let out .= strpart(line, pos)
break
endif
let out .= strpart(line, pos, mstart - pos)
" syntax で保護判定(syntaxが無効な環境ではそのまま大文字化)
let syn = synIDattr(synIDtrans(synID(lnum, mstart + 1, 1)), 'name')
if syn ==# 'String' || syn ==# 'Comment' || syn ==# 'Constant'
let out .= str
else
let out .= toupper(str)
endif
let pos = mend
endwhile
if out !=# line
call setline(lnum, out)
endif
endfor
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/help.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/help.vim
" Module: help — SAS関数/PROC/オプションの辞書ヘルプ
" Description: dict/ の説明フィールドを K キー/:SasHelp から引く。
" +popupwin ならカーソル位置にポップアップ、無ければ echo。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
function! saside#help#Lookup(word) abort
let word = a:word !=# '' ? a:word : expand('<cword>')
if word ==# ''
return
endif
let desc = saside#complete#Describe(word)
if desc ==# ''
call saside#util#Info('辞書に無い語: ' . word)
return
endif
if exists('*popup_atcursor')
call popup_atcursor(desc, {'padding': [0, 1, 0, 1], 'moved': 'any'})
else
echo desc
endif
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/layout.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/layout.vim
" Module: layout — ウィンドウレイアウト管理
" Description: 各ウィンドウに w:saside_role の役割タグを付けて管理する。
" 役割: 'explorer'(左) / 'navigator'(右) / 'log'(下)。
" 役割なしウィンドウが「エディタ」である。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
" 指定roleのウィンドウIDを返す(無ければ -1)
function! saside#layout#Win(role) abort
for nr in range(1, winnr('$'))
if getwinvar(nr, 'saside_role', '') ==# a:role
return win_getid(nr)
endif
endfor
return -1
endfunction
" エディタ(役割なし)ウィンドウのIDを返す
function! saside#layout#Editor() abort
for nr in range(1, winnr('$'))
if getwinvar(nr, 'saside_role', '') ==# ''
return win_getid(nr)
endif
endfor
return win_getid(winnr())
endfunction
" roleのペインを開いて移動する(既にあれば移動のみ)。winidを返す。
function! saside#layout#Open(role) abort
let id = saside#layout#Win(a:role)
if id != -1
call win_gotoid(id)
return id
endif
if a:role ==# 'log'
execute 'botright ' . get(g:, 'saside_log_height', 10) . 'split'
elseif a:role ==# 'explorer'
execute 'topleft vertical ' . get(g:, 'saside_explorer_width', 28) . 'split'
else
execute 'botright vertical ' . get(g:, 'saside_navigator_width', 34) . 'split'
endif
let w:saside_role = a:role
setlocal winfixwidth winfixheight
call saside#event#Emit('LayoutChanged', {'role': a:role, 'action': 'open'})
return win_getid()
endfunction
" roleのペインを閉じる
function! saside#layout#Close(role) abort
let id = saside#layout#Win(a:role)
if id == -1
return v:false
endif
let cur = win_getid()
call win_gotoid(id)
" 最後の1枚は閉じられない
if winnr('$') > 1
close
endif
if cur != id && win_gotoid(cur) != 1
call win_gotoid(saside#layout#Editor())
endif
call saside#event#Emit('LayoutChanged', {'role': a:role, 'action': 'close'})
return v:true
endfunction
" IDE一括レイアウト(左Explorer + 右Navigator + 下Log)。
" フォーカスはエディタに戻す。
"
" 重要: 各パネルを開く前に必ずエディタ(SASバッファ)へ戻る。
" Navigator は「現在ウィンドウのfiletypeがsasか」で対象バッファを判定するため、
" 別パネル(explorer等 ft≠sas)の中から呼ぶと対象を見失って開けない。
function! saside#layout#Ide() abort
let ed = saside#layout#Editor()
" Navigator を最初に、必ずSASバッファがカレントな状態で開く
call win_gotoid(ed)
call saside#navigator#Show('nav')
call win_gotoid(ed)
call saside#explorer#Open()
" ログはファイルがある場合のみ(log#Open内でフォーカスは自己復元)
call win_gotoid(ed)
silent! call saside#log#Open()
call win_gotoid(ed)
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/log.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/log.vim
" Module: log — SASログビューア
" Description: 下ペインにログを表示し、ERROR/WARNING/NOTE の抽出と
" quickfix連携を行う。色分けは syntax/saslog.vim に委譲。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
" 対象ログのパスを決める(引数 > 直近実行 > カレントファイル.log)
function! s:target(args) abort
if len(a:args) > 0 && a:args[0] !=# ''
return fnamemodify(a:args[0], ':p')
endif
if exists('*saside#run#Last') && !empty(saside#run#Last())
return saside#run#Last().log
endif
return expand('%:p:r') . '.log'
endfunction
" ログを下ペインに開く。フォーカスは呼び出し元へ戻す。
function! saside#log#Open(...) abort
let path = s:target(a:000)
if !filereadable(path)
call saside#util#Warn('ログがありません: ' . path)
return v:false
endif
let cur = win_getid()
call saside#layout#Open('log')
execute 'silent edit! ' . fnameescape(path)
setlocal autoread nowrap
setlocal filetype=saslog
normal! G
call win_gotoid(cur)
return v:true
endfunction
" .lst(プロシジャ出力)を下ペインに開く
function! saside#log#Lst(...) abort
let path = len(a:000) > 0 && a:000[0] !=# ''
\ ? fnamemodify(a:000[0], ':p')
\ : (exists('*saside#run#Last') && !empty(saside#run#Last())
\ ? saside#run#Last().lst : expand('%:p:r') . '.lst')
if !filereadable(path)
call saside#util#Warn('lstがありません: ' . path)
return v:false
endif
let cur = win_getid()
call saside#layout#Open('log')
execute 'silent edit! ' . fnameescape(path)
setlocal autoread nowrap filetype=saslst
call win_gotoid(cur)
return v:true
endfunction
" ログペインをトグルする
function! saside#log#Toggle() abort
if saside#layout#Win('log') != -1
call saside#layout#Close('log')
else
call saside#log#Open()
endif
endfunction
" ログをパースして quickfix に流す。kind: 'error'|'warning'|'note'|'all'
function! saside#log#Quickfix(...) abort
let kind = len(a:000) > 0 ? a:000[0] : 'all'
let path = s:target(len(a:000) > 1 ? [a:000[1]] : [])
if !filereadable(path)
call saside#util#Warn('ログがありません: ' . path)
return 0
endif
let pats = {
\ 'error': '^\d*\s*ERROR\%(\s\d\+-\d\+\)\?:',
\ 'warning': '^\d*\s*WARNING:',
\ 'note': '^\d*\s*NOTE:',
\ }
let pat = kind ==# 'all'
\ ? pats.error . '\|' . pats.warning
\ : get(pats, kind, pats.error)
let items = []
let lnum = 0
for line in readfile(path)
let lnum += 1
if line =~# pat
call add(items, {'filename': path, 'lnum': lnum, 'text': line})
endif
endfor
call setqflist([], ' ', {'title': 'SAS log [' . kind . ']', 'items': items})
if !empty(items)
botright cwindow
else
cclose
call saside#util#Info('該当なし (' . kind . ')')
endif
return len(items)
endfunction
" -----------------------------------------------------------------------------
" 危険NOTE検出(SASログスキャナ)
" -----------------------------------------------------------------------------
"
" SASで最も怖いのは ERROR ではなく「NOTE で警告されるだけで処理が続行し、
" 黙って誤った結果を出す」類の事象である。ここではその代表的パターンを
" 辞書化し、理由付きで quickfix に出す。
" g:saside_log_risk_patterns に [{'pat':正規表現, 'desc':説明}] を足せば拡張可。
let s:RISK_PATTERNS = [
\ {'pat': 'uninitialized',
\ 'desc': '変数が未初期化(変数名のタイプミス・定義漏れの可能性)'},
\ {'pat': 'Invalid \%(numeric \|character \)\?data',
\ 'desc': '不正なデータ(型変換に失敗し欠損値になっている)'},
\ {'pat': 'MERGE statement has more than one data set with repeats of BY values',
\ 'desc': 'MERGEでBY値が重複(意図しない結合・件数増加の典型例)'},
\ {'pat': 'Division by zero',
\ 'desc': 'ゼロ除算(結果が欠損値になっている)'},
\ {'pat': 'W\.D format was too small',
\ 'desc': '桁あふれ(表示が丸められ・欠けている)'},
\ {'pat': 'Character values have been converted to numeric',
\ 'desc': '文字→数値の暗黙変換(意図しない型混在)'},
\ {'pat': 'Numeric values have been converted to character',
\ 'desc': '数値→文字の暗黙変換(意図しない型混在)'},
\ {'pat': 'Missing values were generated',
\ 'desc': '演算の結果として欠損値が発生'},
\ {'pat': 'Mathematical operations could not be performed',
\ 'desc': '数学演算を実行できず欠損値が発生'},
\ {'pat': 'has 0 observations',
\ 'desc': '出力が0件(抽出条件・結合条件の誤りの可能性)'},
\ {'pat': 'SAS went to a new line when INPUT statement reached past the end of a line',
\ 'desc': 'INPUTが行をまたいだ(読み込み位置がずれている)'},
\ {'pat': 'LOST CARD',
\ 'desc': '入力データの欠落(レコード構造の不整合)'},
\ {'pat': 'Multiple lengths were specified for the variable',
\ 'desc': '変数長の不一致(文字列が切り捨てられる恐れ)'},
\ {'pat': 'Truncated record',
\ 'desc': 'レコードが切り捨てられた'},
\ ]
" ログから危険NOTEを抽出して quickfix に出す
function! saside#log#Risk(...) abort
let path = s:target(a:000)
if !filereadable(path)
call saside#util#Warn('ログがありません: ' . path)
return 0
endif
let patterns = s:RISK_PATTERNS + get(g:, 'saside_log_risk_patterns', [])
let items = []
let lnum = 0
for line in readfile(path)
let lnum += 1
for p in patterns
if line =~? p.pat
call add(items, {'filename': path, 'lnum': lnum, 'type': 'W',
\ 'text': '[' . p.desc . '] ' . saside#util#Trim(line)})
break
endif
endfor
endfor
call setqflist([], ' ', {'title': 'SAS log 危険NOTE', 'items': items})
if !empty(items)
botright cwindow
call saside#util#Warn('要確認の危険NOTE: ' . len(items) . '件')
else
cclose
call saside#util#Info('危険NOTEはありません')
endif
return len(items)
endfunction
" RunFinished イベントハンドラ(plugin/saside.vim から配線される)
function! saside#log#OnRunFinished(ev) abort
let exit = get(a:ev, 'exit', -1)
let path = get(a:ev, 'log', '')
call saside#log#Open(path)
let n = saside#log#Quickfix('all', path)
" 危険NOTEは件数だけ数えて知らせる(quickfixはERROR/WARNINGを優先表示)
let risk = s:count_risk(path)
if exit == 0 && n == 0 && risk == 0
call saside#util#Info('SAS正常終了 (exit 0)')
elseif exit == 0 && n == 0
call saside#util#Warn(printf(
\ 'SAS終了 exit=0 ですが危険NOTEが %d件あります(:SasLogRisk で確認)',
\ risk))
else
call saside#util#Warn(printf(
\ 'SAS終了 exit=%d / ERROR+WARNING %d件 / 危険NOTE %d件', exit, n, risk))
endif
endfunction
" 危険NOTEの件数だけを数える(quickfixを書き換えない)
function! s:count_risk(path) abort
if !filereadable(a:path)
return 0
endif
let patterns = s:RISK_PATTERNS + get(g:, 'saside_log_risk_patterns', [])
let n = 0
for line in readfile(a:path)
for p in patterns
if line =~? p.pat
let n += 1
break
endif
endfor
endfor
return n
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/macro.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/macro.vim
" Module: macro — Macro Browser(navigator の表示モード委譲)
" =============================================================================
function! saside#macro#Toggle() abort
call saside#navigator#Toggle('macro')
endfunction
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/navigator.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/navigator.vim
" Module: navigator — 右ペインのコード構造ビュー(共通描画基盤)
" Description: parser のインデックスを4つのモードで表示する。
" nav : 種別ごとにグループ化(SAS Navigator)
" outline : ファイル出現順の平坦リスト(Outline)
" dataset : 入出力データセット一覧(Dataset Browser)
" macro : マクロ定義一覧(Macro Browser)
" Enterでソース位置へジャンプ。IndexUpdatedで自動再描画。
" outline/dataset/macro モジュールは本モジュールの thin wrapper。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
let s:mode = 'nav'
let s:src = -1 " 表示対象のSASバッファ
let s:NAV_ORDER = ['macro', 'data', 'proc', 'libname', 'filename',
\ 'options', 'ods', 'title', 'footnote', 'label']
" ペインを開いて指定モードで描画する
function! saside#navigator#Show(mode) abort
let s:mode = a:mode
if &filetype ==# 'sas'
let s:src = bufnr('%')
endif
if s:src == -1 || !bufexists(s:src)
call saside#util#Warn('SASバッファがありません')
return v:false
endif
call saside#layout#Open('navigator')
let bufnr = saside#util#Scratch('navigator')
call s:render(bufnr)
call s:setup_buffer()
return v:true
endfunction
" トグル(同モードで開いていれば閉じる)
function! saside#navigator#Toggle(mode) abort
if saside#layout#Win('navigator') != -1 && s:mode ==# a:mode
call saside#layout#Close('navigator')
else
call saside#navigator#Show(a:mode)
endif
endfunction
" 再描画(ペインが開いている時のみ)
function! saside#navigator#Refresh() abort
let winid = saside#layout#Win('navigator')
if winid == -1
return
endif
let bufnr = bufnr('saside://navigator')
if bufnr != -1
call s:render(bufnr)
endif
endfunction
" IndexUpdated イベントハンドラ(plugin から配線)
function! saside#navigator#OnIndexUpdated(ev) abort
if get(a:ev, 'bufnr', -1) ==# s:src
call saside#navigator#Refresh()
endif
endfunction
" 現在行の項目へジャンプする
function! saside#navigator#Jump() abort
let items = get(b:, 'saside_items', [])
let item = get(items, line('.') - 1, {})
if empty(item)
return
endif
call win_gotoid(saside#layout#Editor())
if bufnr('%') != s:src
execute 'silent buffer ' . s:src
endif
call cursor(item.lnum, 1)
normal! zz
if get(g:, 'saside_nav_stay', 0)
call win_gotoid(saside#layout#Win('navigator'))
endif
endfunction
" -----------------------------------------------------------------------------
" 描画
" -----------------------------------------------------------------------------
function! s:render(bufnr) abort
let idx = saside#parser#Index(s:src)
let lines = []
let items = [] " lines と平行。ヘッダ行は {}
let title = {'nav': 'NAVIGATOR', 'outline': 'OUTLINE',
\ 'dataset': 'DATASETS', 'macro': 'MACROS'}[s:mode]
call add(lines, printf('%s [%s]', title, fnamemodify(bufname(s:src), ':t')))
call add(items, {})
if s:mode ==# 'nav'
for type in s:NAV_ORDER
let group = filter(copy(idx.blocks), 'v:val.type ==# type')
if empty(group)
continue
endif
call add(lines, '▸ ' . toupper(type) . ' (' . len(group) . ')')
call add(items, {})
for b in group
call add(lines, printf(' %-28s :%d', strpart(s:name(b), 0, 28), b.lnum))
call add(items, b)
endfor
endfor
elseif s:mode ==# 'outline'
for b in idx.blocks
call add(lines, printf(' %-8s %-24s :%d',
\ b.type, strpart(s:name(b), 0, 24), b.lnum))
call add(items, b)
endfor
elseif s:mode ==# 'dataset'
for io in ['out', 'in']
let group = filter(copy(idx.datasets), 'v:val.io ==# io')
call add(lines, '▸ ' . (io ==# 'out' ? 'OUTPUT' : 'INPUT')
\ . ' (' . len(group) . ')')
call add(items, {})
for d in group
call add(lines, printf(' %-22s via %-6s :%d',
\ strpart(d.name, 0, 22), d.via, d.lnum))
call add(items, d)
endfor
endfor
elseif s:mode ==# 'macro'
for m in idx.macros
call add(lines, printf(' %%%s(%s) :%d',
\ m.name, join(m.args, ', '), m.lnum))
call add(items, m)
endfor
if empty(idx.macros)
call add(lines, ' (マクロ定義なし)')
call add(items, {})
endif
endif
call saside#util#SetLines(a:bufnr, lines)
call setbufvar(a:bufnr, 'saside_items', items)
endfunction
" ブロックの表示名(空なら種別で代用)
function! s:name(b) abort
return a:b.name !=# '' ? a:b.name : '(' . a:b.type . ')'
endfunction
" ナビゲータバッファのローカル設定(初回のみ有効化される)
function! s:setup_buffer() abort
if get(b:, 'saside_nav_ready', 0)
return
endif
let b:saside_nav_ready = 1
setlocal cursorline
nnoremap <buffer> <silent> <CR> :call saside#navigator#Jump()<CR>
nnoremap <buffer> <silent> q :call saside#layout#Close('navigator')<CR>
nnoremap <buffer> <silent> r :call saside#navigator#Refresh()<CR>
nnoremap <buffer> <silent> 1 :call saside#navigator#Show('nav')<CR>
nnoremap <buffer> <silent> 2 :call saside#navigator#Show('outline')<CR>
nnoremap <buffer> <silent> 3 :call saside#navigator#Show('dataset')<CR>
nnoremap <buffer> <silent> 4 :call saside#navigator#Show('macro')<CR>
" 簡易ハイライト
syntax match SasideNavHeader /^[▸A-Z].*/
syntax match SasideNavLnum /:\d\+$/
highlight default link SasideNavHeader Title
highlight default link SasideNavLnum LineNr
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/outline.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/outline.vim
" Module: outline — Outline ビュー(navigator の表示モード委譲)
" =============================================================================
function! saside#outline#Toggle() abort
call saside#navigator#Toggle('outline')
endfunction
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/parser.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/parser.vim
" Module: parser — SASIDE の中核。SASコードのインデックス生成
" Description: バッファを1パス走査し、DATA/PROC/%MACRO/LIBNAME/ODS等の
" 「ブロック一覧」、入出力データセット一覧、マクロ定義、
" マクロ変数を抽出した dict(コードインデックス)を返す。
" Navigator/Outline/Dataset/Macro/StatusLine/補完のすべてが
" このインデックスだけを参照する(単一パーサ・複数ビュー)。
"
" 設計メモ:
" - 正規表現ベースの行スキャナである。/* */ 複数行コメントは状態機械で
" スキップする。文字列内のキーワード誤検出は文字列除去で回避する。
" - キャッシュは b:saside_index に保持し b:changedtick で無効化判定する。
" - 完全な構文解析ではない。SASの全構文網羅より「実務コードで外さない」
" ことを優先している(既知の限界は doc/saside.txt に明記)。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
" -----------------------------------------------------------------------------
" 公開API
" -----------------------------------------------------------------------------
" コードインデックスを返す(キャッシュ有効)。
function! saside#parser#Index(...) abort
let bufnr = a:0 ? a:1 : bufnr('%')
let tick = getbufvar(bufnr, 'changedtick', -1)
let cache = getbufvar(bufnr, 'saside_index', {})
if !empty(cache) && get(cache, 'tick', -2) ==# tick
return cache
endif
let idx = s:scan(getbufline(bufnr, 1, '$'))
let idx.tick = tick
let idx.bufnr = bufnr
call setbufvar(bufnr, 'saside_index', idx)
" イベントはtimerで遅延発火する。Index()はステータスライン評価などの
" textlock文脈(テキスト/ウィンドウ変更禁止)から呼ばれ得るため、
" その場でEmitするとハンドラのUI更新が E565 になる
if has('timers')
call timer_start(0,
\ {-> saside#event#Emit('IndexUpdated', {'bufnr': bufnr})})
else
call saside#event#Emit('IndexUpdated', {'bufnr': bufnr})
endif
return idx
endfunction
" キャッシュ済みインデックスを返す(解析は一切しない読み取り専用版)。
" 未解析なら空dict。textlock文脈(statusline等)からはこちらを使うこと。
function! saside#parser#Cached(...) abort
let bufnr = a:0 ? a:1 : bufnr('%')
return getbufvar(bufnr, 'saside_index', {})
endfunction
" キャッシュを破棄する(次回 Index() で必ず再解析)
function! saside#parser#Invalidate(...) abort
let bufnr = a:0 ? a:1 : bufnr('%')
call setbufvar(bufnr, 'saside_index', {})
endfunction
" 指定行を含む最内ブロックを返す(無ければ空dict)。statusline用。
function! saside#parser#BlockAt(lnum, ...) abort
let bufnr = a:0 ? a:1 : bufnr('%')
let best = {}
for b in saside#parser#Index(bufnr).blocks
if b.lnum <= a:lnum && a:lnum <= b.end
" より内側(開始行が大きい)のブロックを優先する
if empty(best) || b.lnum >= best.lnum
let best = b
endif
endif
endfor
return best
endfunction
" 予約語テーブルを返す(format/complete が共有。カテゴリ→語リスト)
function! saside#parser#Keywords() abort
return s:keywords
endfunction
" -----------------------------------------------------------------------------
" 定数
" -----------------------------------------------------------------------------
let s:keywords = {
\ 'statement': ['data','proc','run','quit','set','merge','update',
\ 'modify','by','where','output','input','infile','file','if','then',
\ 'else','do','end','until','while','select','when','otherwise',
\ 'return','array','retain','drop','keep','rename','length','format',
\ 'informat','label','title','footnote','options','libname',
\ 'filename','call','put','putlog','cards','datalines','attrib','ods'],
\ 'macro': ['%macro','%mend','%let','%put','%if','%then','%else','%do',
\ '%end','%to','%until','%while','%global','%local','%return',
\ '%include','%sysfunc','%sysevalf','%eval','%scan','%substr',
\ '%index','%length','%upcase','%str','%nrstr','%bquote','%quote'],
\ }
" ブロック終端とみなす文(run; / quit;)
let s:re_close = '\c^\s*\%(run\|quit\)\s*;'
" 単一行で完結する宣言系ステートメント
let s:re_oneline = '\c^\s*\(libname\|filename\|options\|title\d*\|footnote\d*\|ods\|%include\)\>\s*\([^;]*\)'
" ラベル(goto先): name: 形式。ただし予約語や代入と紛れないよう先頭のみ
let s:re_label = '^\s*\(\a\w*\)\s*:\s*$'
" -----------------------------------------------------------------------------
" スキャナ本体
" -----------------------------------------------------------------------------
" 行リストを走査してインデックスを構築する
function! s:scan(lines) abort
let idx = {'blocks': [], 'datasets': [], 'macros': [], 'macrovars': []}
let mvars = {}
" 状態
let in_comment = 0 " /* */ コメント内
let cur = {} " 現在開いている data/proc ブロック
let macro_stack = [] " ネスト中の %macro ブロック
let in_sql = 0 " PROC SQL 内(FROM/JOIN抽出用)
let lnum = 0
for raw in a:lines
let lnum += 1
" withstr: コメント除去済み・文字列リテラルは保持(symput抽出用)
" line: さらに文字列の中身を潰したもの(構造マッチ用)
let [withstr, in_comment] = s:strip_noise(raw, in_comment)
let line = s:squash_strings(withstr)
if line =~ '^\s*$'
continue
endif
" ---- %macro / %mend (data/procより先に判定)----
let m = matchlist(line, '\c^\s*%macro\s\+\(\w\+\)\s*\%((\([^)]*\))\)\?')
if !empty(m)
let blk = {'type': 'macro', 'name': m[1], 'lnum': lnum, 'end': lnum,
\ 'args': s:parse_args(m[2])}
call add(idx.blocks, blk)
call add(idx.macros, blk)
call add(macro_stack, blk)
" マクロ引数もマクロ変数として補完対象に載せる
for a in blk.args
let mvars[a] = 1
endfor
continue
endif
if line =~ '\c^\s*%mend\>'
if !empty(macro_stack)
let macro_stack[-1].end = lnum
call remove(macro_stack, -1)
endif
continue
endif
" ---- %let ----
let m = matchlist(line, '\c%let\s\+\(\w\+\)')
if !empty(m)
let mvars[m[1]] = 1
endif
" ---- call symput / symputx (文字列を保持した withstr からマッチ)----
let m = matchlist(withstr, '\ccall\s\+symputx\?\s*(\s*[''"]\(\w\+\)[''"]')
if !empty(m)
let mvars[m[1]] = 1
endif
" ---- select ... into :var ----
let m = matchlist(line, '\cinto\s*:\s*\(\w\+\)')
if !empty(m)
let mvars[m[1]] = 1
endif
" ---- DATAステップ開始 ----
let m = matchlist(line, '\c^\s*data\s\+\([^;(]\+\)')
if !empty(m) && line !~ '\c^\s*data\s*='
call s:close_block(cur, lnum - 1)
let names = s:split_ds_names(m[1])
let cur = {'type': 'data', 'name': join(names, ' '), 'lnum': lnum,
\ 'end': lnum}
call add(idx.blocks, cur)
for n in names
call add(idx.datasets,
\ {'name': n, 'io': 'out', 'via': 'data', 'lnum': lnum})
endfor
let in_sql = 0
continue
endif
" data _null_; のような ( や ; 直結パターン
if line =~ '\c^\s*data\s*;'
call s:close_block(cur, lnum - 1)
let cur = {'type': 'data', 'name': '(unnamed)', 'lnum': lnum, 'end': lnum}
call add(idx.blocks, cur)
let in_sql = 0
continue
endif
" ---- PROC開始 ----
let m = matchlist(line, '\c^\s*proc\s\+\(\w\+\)')
if !empty(m)
call s:close_block(cur, lnum - 1)
let cur = {'type': 'proc', 'name': tolower(m[1]), 'lnum': lnum,
\ 'end': lnum}
call add(idx.blocks, cur)
let in_sql = (tolower(m[1]) ==# 'sql')
" proc xxx data=yyy の入力データセット
let dm = matchlist(line, '\cdata\s*=\s*\([A-Za-z_&][A-Za-z0-9_.&]*\)')
if !empty(dm)
call add(idx.datasets,
\ {'name': dm[1], 'io': 'in', 'via': 'proc', 'lnum': lnum})
endif
continue
endif
" ---- run; / quit; でブロックを閉じる ----
if line =~ s:re_close
call s:close_block(cur, lnum)
let cur = {}
let in_sql = 0
continue
endif
" ---- ブロック内の入力データセット ----
let m = matchlist(line, '\c^\s*\(set\|merge\|update\|modify\)\s\+\([^;]*\)')
if !empty(m)
for n in s:split_ds_names(m[2])
call add(idx.datasets,
\ {'name': n, 'io': 'in', 'via': tolower(m[1]), 'lnum': lnum})
endfor
continue
endif
" ---- PROC SQL: create table / from / join ----
if in_sql
let m = matchlist(line, '\c\<create\s\+table\s\+\([A-Za-z_&][A-Za-z0-9_.&]*\)')
if !empty(m)
call add(idx.datasets,
\ {'name': m[1], 'io': 'out', 'via': 'create', 'lnum': lnum})
endif
let m = matchlist(line, '\c\<from\s\+\([A-Za-z_&][A-Za-z0-9_.&]*\)')
if !empty(m)
call add(idx.datasets,
\ {'name': m[1], 'io': 'in', 'via': 'from', 'lnum': lnum})
endif
let m = matchlist(line, '\c\<join\s\+\([A-Za-z_&][A-Za-z0-9_.&]*\)')
if !empty(m)
call add(idx.datasets,
\ {'name': m[1], 'io': 'in', 'via': 'join', 'lnum': lnum})
endif
endif
" ---- 単一行宣言(libname/filename/options/title/footnote/ods)----
let m = matchlist(line, s:re_oneline)
if !empty(m)
call add(idx.blocks, {'type': tolower(substitute(m[1], '\d\+$', '', '')),
\ 'name': saside#util#Trim(m[2]), 'lnum': lnum, 'end': lnum})
continue
endif
" ---- ラベル ----
let m = matchlist(line, s:re_label)
if !empty(m)
call add(idx.blocks,
\ {'type': 'label', 'name': m[1], 'lnum': lnum, 'end': lnum})
endif
endfor
" ファイル末尾まで閉じられなかったブロックの後始末
call s:close_block(cur, lnum)
for blk in macro_stack
let blk.end = lnum
endfor
let idx.macrovars = sort(keys(mvars))
return idx
endfunction
" -----------------------------------------------------------------------------
" 内部ヘルパ
" -----------------------------------------------------------------------------
" コメントを行から除去する(文字列リテラルは保持する)。
" 戻り値: [処理済みの行, 次行に持ち越す in_comment 状態]
function! s:strip_noise(line, in_comment) abort
let line = a:line
let in_comment = a:in_comment
if in_comment
let pos = stridx(line, '*/')
if pos == -1
return ['', 1]
endif
let line = strpart(line, pos + 2)
let in_comment = 0
endif
" 同一行で閉じる /* */ を除去(複数個対応)
let line = substitute(line, '/\*.\{-}\*/', ' ', 'g')
" 行内で開いたまま閉じない /* は以降を捨てて持ち越す
let pos = stridx(line, '/*')
if pos != -1
let line = strpart(line, 0, pos)
let in_comment = 1
endif
" 行頭 * コメント(; まで。閉じ ; が無ければ行全体を捨てる)
if line =~ '^\s*\*'
let line = substitute(line, '^\s*\*[^;]*;\?', '', '')
endif
return [line, in_comment]
endfunction
" 文字列リテラルの中身を '@' に潰す(構造マッチでのキーワード誤検出防止)
function! s:squash_strings(line) abort
let line = substitute(a:line, "'[^']*'", "'@'", 'g')
return substitute(line, '"[^"]*"', '"@"', 'g')
endfunction
" %macro の引数リストを分解する("a=, b, c=x" → ['a','b','c'])
function! s:parse_args(argstr) abort
let args = []
for part in split(a:argstr, ',')
let name = matchstr(part, '\w\+')
if name !=# ''
call add(args, name)
endif
endfor
return args
endfunction
" "work.a raw.b(keep=x)" のようなデータセット名並びを分解する
function! s:split_ds_names(str) abort
let names = []
" 括弧内のオプションを除去してから空白区切りで分解
let clean = substitute(a:str, '([^)]*)', '', 'g')
for tok in split(clean, '\s\+')
let tok = saside#util#Trim(tok)
" データセット名として妥当なものだけ(key=value の残骸などを除外)。
" & を含むのはマクロ変数展開名(例: work_l.&outdata.)を許容するため
if tok =~# '^[A-Za-z_&][A-Za-z0-9_.&]*$' && tok !~? '^\%(end\|nobs\|key\)$'
call add(names, tok)
endif
endfor
return names
endfunction
" 開いているブロックの終端行を確定する
function! s:close_block(blk, lnum) abort
if !empty(a:blk) && a:lnum >= a:blk.lnum
let a:blk.end = a:lnum
endif
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/run.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/run.vim
" Module: run — SASバッチ実行
" Description: 保存→実行→終了コード判定→イベント発火。実行方式は
" +job(非同期)→ system(同期)の順で自動選択する。
" 端末で対話的に実行したい場合は RunTerm()(+terminal時のみ)。
" Events: RunStarted {file, mode} / RunFinished {exit, log, lst}
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
let s:job = v:null
let s:status = 'idle' " 'idle' | 'running' | 'done:N'
let s:last = {} " {'file':..,'log':..,'lst':..}
" 直近の実行情報を返す(logモジュールが利用)
function! saside#run#Last() abort
return s:last
endfunction
" 現在の実行状態を返す
function! saside#run#Status() abort
return s:status
endfunction
" 構文チェック実行(obs=0)。
" データを1件も処理せずに構文だけ検証するSASの定石。巨大データを読む
" プログラムでも数秒でタイプミスを検出できる。-noreplace により永久
" データセットの上書きも防ぐ。
function! saside#run#Check() abort
let args = get(g:, 'saside_check_args', ['-obs', '0', '-noreplace'])
call saside#util#Info('構文チェック実行中(' . join(args, ' ') . ')')
return saside#run#Execute('', args)
endfunction
" SASをバッチ実行する。
" a:1 対象ファイル(省略/空なら現在バッファ)
" a:2 追加コマンドライン引数のリスト(省略可)
function! saside#run#Execute(...) abort
if s:status ==# 'running'
call saside#util#Warn('SAS実行中です(中断は :SasAbort)')
return {'started': v:false}
endif
let file = a:0 && a:1 !=# '' ? fnamemodify(a:1, ':p') : expand('%:p')
if file ==# ''
call saside#util#Error('実行対象ファイルがありません')
return {'started': v:false}
endif
" 現在バッファ対象なら未保存変更を保存
if file ==# expand('%:p') && &modified
silent update
endif
let base = fnamemodify(file, ':r')
let logf = base . '.log'
let lstf = base . '.lst'
let s:last = {'file': file, 'log': logf, 'lst': lstf}
let extra = a:0 >= 2 ? a:2 : []
let cmd = [g:saside_sas_cmd, '-sysin', file, '-log', logf, '-print', lstf]
\ + g:saside_sas_args + extra
if saside#util#Capabilities().job
let s:status = 'running'
call saside#event#Emit('RunStarted', {'file': file, 'mode': 'job'})
let s:job = job_start(cmd, {'exit_cb': function('s:on_exit')})
if job_status(s:job) ==# 'fail'
let s:status = 'idle'
call saside#util#Error('起動に失敗: ' . g:saside_sas_cmd)
return {'started': v:false}
endif
call saside#util#Info('SAS実行開始: ' . fnamemodify(file, ':t'))
return {'started': v:true, 'mode': 'job'}
endif
" 同期フォールバック
call saside#event#Emit('RunStarted', {'file': file, 'mode': 'sync'})
call system(join(map(copy(cmd), 'shellescape(v:val)'), ' '))
let s:status = 'done:' . v:shell_error
call saside#event#Emit('RunFinished',
\ {'exit': v:shell_error, 'log': logf, 'lst': lstf})
return {'started': v:true, 'mode': 'sync'}
endfunction
" -----------------------------------------------------------------------------
" 部分実行(選択範囲 / カーソル位置のブロック)
" -----------------------------------------------------------------------------
" 選択範囲を一時ファイルに書き出して実行する。
" 一時ファイルは元ファイルと同じディレクトリに作る(相対パスのLIBNAMEや
" %include が元と同じ条件で解決されるようにするため)。
" g:saside_run_preamble=1(既定)なら、選択範囲より前にある
" LIBNAME/FILENAME/OPTIONS/%LET を自動で先頭に付ける(部分実行でも
" ライブラリ参照やマクロ変数が解決できるようにするため)。
function! saside#run#ExecuteRange(line1, line2) abort
let lines = getline(a:line1, a:line2)
if empty(filter(copy(lines), 'v:val !~ "^\\s*$"'))
call saside#util#Error('選択範囲が空です')
return {'started': v:false}
endif
let src = expand('%:p')
let dir = src !=# '' ? fnamemodify(src, ':h') : getcwd()
let base = src !=# '' ? fnamemodify(src, ':t:r') : 'untitled'
let tmp = dir . '/' . base . '_sel.sas'
let body = []
if get(g:, 'saside_run_preamble', 1)
let body += s:preamble(a:line1)
endif
let body += lines
if writefile(body, tmp) != 0
call saside#util#Error('一時ファイルを作成できません: ' . tmp)
return {'started': v:false}
endif
call saside#util#Info(printf('部分実行: %d-%d行 (%d行送信)',
\ a:line1, a:line2, len(body)))
return saside#run#Execute(tmp)
endfunction
" カーソル位置のブロック(DATA/PROC/%MACRO)だけを実行する
function! saside#run#ExecuteBlock() abort
let blk = saside#parser#BlockAt(line('.'))
if empty(blk)
call saside#util#Warn('カーソル位置にDATA/PROCブロックがありません')
return {'started': v:false}
endif
call saside#util#Info(printf('ブロック実行: %s %s (L%d-%d)',
\ toupper(blk.type), blk.name, blk.lnum, blk.end))
return saside#run#ExecuteRange(blk.lnum, blk.end)
endfunction
" 指定行より前にある LIBNAME/FILENAME/OPTIONS/%LET を集める
function! s:preamble(before_lnum) abort
let out = []
if a:before_lnum <= 1
return out
endif
for line in getline(1, a:before_lnum - 1)
if line =~? '^\s*\%(libname\|filename\|options\)\>'
\ || line =~? '^\s*%let\>'
call add(out, line)
endif
endfor
if !empty(out)
call insert(out, '/* --- saside: auto preamble (前方のLIBNAME/OPTIONS/%LET) --- */')
call add(out, '/* --- end preamble --- */')
call add(out, '')
endif
return out
endfunction
" +terminal 環境では端末ウィンドウ内で実行する(進行が見える)
function! saside#run#ExecuteTerm(...) abort
if !has('terminal')
return saside#run#Execute(a:0 ? a:1 : '')
endif
let file = a:0 && a:1 !=# '' ? fnamemodify(a:1, ':p') : expand('%:p')
if file ==# expand('%:p') && &modified
silent update
endif
let base = fnamemodify(file, ':r')
let s:last = {'file': file, 'log': base . '.log', 'lst': base . '.lst'}
let cmd = [g:saside_sas_cmd, '-sysin', file,
\ '-log', s:last.log, '-print', s:last.lst] + g:saside_sas_args
botright call term_start(cmd, {'term_rows': get(g:, 'saside_log_height', 10),
\ 'exit_cb': function('s:on_exit')})
let s:status = 'running'
call saside#event#Emit('RunStarted', {'file': file, 'mode': 'term'})
return {'started': v:true, 'mode': 'term'}
endfunction
" 実行中ジョブを停止する
function! saside#run#Abort() abort
if s:status !=# 'running' || type(s:job) != v:t_job
call saside#util#Info('実行中のジョブはありません')
return v:false
endif
call job_stop(s:job)
return v:true
endfunction
" ジョブ終了コールバック
function! s:on_exit(job, exit) abort
let s:status = 'done:' . a:exit
call saside#event#Emit('RunFinished',
\ {'exit': a:exit, 'log': get(s:last, 'log', ''),
\ 'lst': get(s:last, 'lst', '')})
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/search.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/search.vim
" Module: search — プロジェクト横断検索(vimgrep ラッパ)
" Description: プロジェクトルートは g:saside_project_root >
" .sasproject マーカーの上方探索 > 現在ファイルのディレクトリ。
" 対象は **/*.sas と **/*.txt(会社運用に合わせる)。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
" プロジェクトルートを返す
function! saside#search#Root() abort
if get(g:, 'saside_project_root', '') !=# ''
return fnamemodify(g:saside_project_root, ':p')
endif
let marker = findfile('.sasproject', expand('%:p:h') . ';')
if marker !=# ''
return fnamemodify(marker, ':p:h')
endif
let dir = expand('%:p:h')
return dir !=# '' ? dir : getcwd()
endfunction
function! s:globs() abort
let root = fnameescape(saside#search#Root())
return root . '/**/*.sas ' . root . '/**/*.txt'
endfunction
" パターンでプロジェクト検索
function! saside#search#Grep(pattern) abort
if a:pattern ==# ''
return
endif
try
execute 'noautocmd vimgrep /' . escape(a:pattern, '/') . '/gj ' . s:globs()
catch /E480/
call saside#util#Info('該当なし: ' . a:pattern)
return
endtry
botright cwindow
endfunction
" データセット名の使用箇所を検索(定義/入出力の行に絞る)
function! saside#search#Dataset(name) abort
let name = a:name !=# '' ? a:name : expand('<cword>')
call saside#search#Grep(
\ '\c\<\%(data\|set\|merge\|update\|modify\|from\|join\|table\)\>'
\ . '[^;]*\<' . name . '\>')
endfunction
" マクロの定義と呼び出しを検索
function! saside#search#Macro(name) abort
let name = a:name !=# '' ? a:name : expand('<cword>')
let name = substitute(name, '^%', '', '')
call saside#search#Grep('%\%(macro\s\+\)\?' . name . '\>')
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/session.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/session.vim
" Module: session — セッション保存/復元
" Description: :mksession のラッパ。保存先は ~/.vim/.state/。
" SASIDEの特殊ペイン(scratchバッファ)はセッションに含めず、
" 復元後に開いていたロール情報から再構築する。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
function! s:path(name) abort
return saside#util#StateDir() . '/' . a:name . '.session.vim'
endfunction
function! s:meta(name) abort
return saside#util#StateDir() . '/' . a:name . '.session.json'
endfunction
function! saside#session#Save(...) abort
let name = a:0 && a:1 !=# '' ? a:1 : 'default'
" 開いているSASIDEペインを記録してから閉じる(scratchはmksession不能)
let roles = []
for role in ['explorer', 'navigator', 'log']
if saside#layout#Win(role) != -1
call add(roles, role)
call saside#layout#Close(role)
endif
endfor
let sso = &sessionoptions
set sessionoptions=blank,buffers,curdir,folds,tabpages,winsize
try
execute 'mksession! ' . fnameescape(s:path(name))
call writefile([json_encode({'roles': roles})], s:meta(name))
finally
let &sessionoptions = sso
endtry
" 閉じたペインを開き直す
for role in roles
call s:reopen(role)
endfor
call saside#util#Info('セッション保存: ' . name)
endfunction
function! saside#session#Load(...) abort
let name = a:0 && a:1 !=# '' ? a:1 : 'default'
if !filereadable(s:path(name))
call saside#util#Error('セッションがありません: ' . name)
return v:false
endif
execute 'silent! source ' . fnameescape(s:path(name))
if filereadable(s:meta(name))
try
let meta = json_decode(join(readfile(s:meta(name)), ''))
for role in get(meta, 'roles', [])
call s:reopen(role)
endfor
catch
endtry
endif
call win_gotoid(saside#layout#Editor())
call saside#util#Info('セッション復元: ' . name)
return v:true
endfunction
function! saside#session#ListNames() abort
return map(glob(saside#util#StateDir() . '/*.session.vim', 1, 1),
\ "fnamemodify(v:val, ':t:r:r')")
endfunction
function! s:reopen(role) abort
if a:role ==# 'explorer'
call saside#explorer#Open()
elseif a:role ==# 'navigator'
call saside#navigator#Show('nav')
elseif a:role ==# 'log'
silent! call saside#log#Open()
endif
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/snippet.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/snippet.vim
" Module: snippet — スニペット展開
" Description: dict/snippets.txt を読み、挿入モードの <C-j> で
" 「カーソル直前の語がスニペットキーなら展開 /
" そうでなければ次の <+placeholder+> へジャンプ」する。
" プレースホルダはジャンプ時に削除され、そこから入力を続ける。
" File format (dict/snippets.txt):
" snippet キー 説明
" 本文行...(次の 'snippet ' 行まで)
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
let s:snips = {}
let s:loaded = 0
function! s:load() abort
if s:loaded
return
endif
let s:loaded = 1
let found = globpath(&runtimepath, 'dict/snippets.txt', 0, 1)
if empty(found)
return
endif
let key = ''
for line in readfile(found[0])
let m = matchlist(line, '^snippet\s\+\(\S\+\)\%(\s\+\(.*\)\)\?$')
if !empty(m)
let key = m[1]
let s:snips[key] = {'desc': m[2], 'body': []}
elseif key !=# '' && line !~# '^\s*#'
call add(s:snips[key].body, line)
endif
endfor
" 末尾の空行を除去
for k in keys(s:snips)
while !empty(s:snips[k].body) && s:snips[k].body[-1] =~# '^\s*$'
call remove(s:snips[k].body, -1)
endwhile
endfor
endfunction
" スニペット一覧(テンプレート/ドキュメント用)
function! saside#snippet#List() abort
call s:load()
return map(sort(keys(s:snips)),
\ {_, k -> printf('%-12s %s', k, s:snips[k].desc)})
endfunction
" <C-j> 本体: 展開 or プレースホルダジャンプ
function! saside#snippet#ExpandOrJump() abort
call s:load()
let line = getline('.')
let col = col('.')
let before = strpart(line, 0, col - 1)
let key = matchstr(before, '\w\+$')
if key !=# '' && has_key(s:snips, key)
call s:expand(key, line, col, before)
else
call saside#snippet#JumpNext()
endif
return ''
endfunction
function! s:expand(key, line, col, before) abort
let snip = s:snips[a:key]
let lnum = line('.')
let indent = matchstr(a:line, '^\s*')
let head = strpart(a:before, 0, len(a:before) - len(a:key))
let tail = strpart(a:line, a:col - 1)
let body = copy(snip.body)
if empty(body)
return
endif
" 1行目はキーの位置に流し込み、2行目以降は同じインデントで挿入する
call setline(lnum, head . body[0])
if len(body) > 1
call append(lnum, map(body[1:], {_, l -> l ==# '' ? '' : indent . l}))
endif
" 末尾に元の後続テキストを繋げる
if tail !=# ''
let last = lnum + len(body) - 1
call setline(last, getline(last) . tail)
endif
call cursor(lnum, max([1, len(head)]))
call saside#snippet#JumpNext()
endfunction
" 次の <+placeholder+> を探して削除し、カーソルをそこへ置く
function! saside#snippet#JumpNext() abort
let pos = searchpos('<+[^+>]*+>', 'cW')
if pos[0] == 0
return v:false
endif
let [lnum, col] = pos
let line = getline(lnum)
let ph = matchstr(line, '<+[^+>]*+>', col - 1)
let name = matchstr(ph, '<+\zs[^+>]*\ze+>')
call setline(lnum,
\ strpart(line, 0, col - 1) . strpart(line, col - 1 + len(ph)))
call cursor(lnum, col)
if name !=# ''
echo '[snippet] ' . name
endif
return v:true
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/statusline.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/statusline.vim
" Module: statusline — 現在ブロック名・実行状態付きステータスライン
" Description: SASバッファに setlocal statusline を適用する。
" 表示: ファイル | 変更 | 現在ブロック(DATA/PROC名) |
" 実行状態 | ft/enc | 行:桁/割合
"
" 重要: statusline の %! 式は再描画中(textlock)に評価されるため、
" この中から解析やイベント発火に到達してはならない(E565になる)。
" Build() は parser#Cached() による読み取り専用参照だけを行い、
" 初回解析は Enable() が timer で予約する。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
let s:run_flag = ''
" FileType sas から呼ばれる(plugin/saside.vim が配線)
function! saside#statusline#Enable() abort
setlocal statusline=%!saside#statusline#Build()
" 初回インデックスを textlock の外で構築しておく
if has('timers')
let bufnr = bufnr('%')
call timer_start(50,
\ {-> bufexists(bufnr) ? saside#parser#Index(bufnr) : 0})
endif
endfunction
" 実行イベントで状態フラグを更新する
function! saside#statusline#OnRun(ev) abort
if a:ev.name ==# 'RunStarted'
let s:run_flag = '[SAS RUNNING]'
else
let exit = get(a:ev, 'exit', -1)
let s:run_flag = exit == 0 ? '[SAS OK]' : '[SAS EXIT ' . exit . ']'
endif
redrawstatus!
endfunction
" statusline 文字列を構築する(読み取り専用。解析は行わない)
function! saside#statusline#Build() abort
let block = ''
if &filetype ==# 'sas'
let idx = saside#parser#Cached()
if !empty(idx)
let lnum = line('.')
let best = {}
for b in get(idx, 'blocks', [])
if b.lnum <= lnum && lnum <= b.end
\ && (empty(best) || b.lnum >= best.lnum)
let best = b
endif
endfor
if !empty(best)
let block = ' [' . toupper(best.type)
\ . (best.name !=# '' ? ' ' . best.name : '') . ']'
endif
endif
endif
return '%f %m%r' . s:esc(block) . ' ' . s:esc(s:run_flag)
\ . ' %= %y %{&fileencoding} %l:%c %p%%'
endfunction
" statusline の % を無害化する
function! s:esc(s) abort
return substitute(a:s, '%', '%%', 'g')
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/template.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/template.vim
" Module: template — :SasNew {kind} でコード骨格を挿入
" Description: snippet と同じプレースホルダ記法 <+name+> を使う。
" 挿入後は snippet#JumpNext で最初のプレースホルダへ移動する。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
let s:templates = {
\ 'data': [
\ 'data <+out+>;',
\ ' set <+in+>;',
\ ' <+statements+>',
\ 'run;',
\ ],
\ 'sql': [
\ 'proc sql;',
\ ' create table <+out+> as',
\ ' select <+columns+>',
\ ' from <+in+>',
\ ' where <+condition+>;',
\ 'quit;',
\ ],
\ 'sort': [
\ 'proc sort data=<+in+> out=<+out+>;',
\ ' by <+keys+>;',
\ 'run;',
\ ],
\ 'means': [
\ 'proc means data=<+in+> n mean std min max maxdec=2;',
\ ' class <+class+>;',
\ ' var <+vars+>;',
\ 'run;',
\ ],
\ 'freq': [
\ 'proc freq data=<+in+>;',
\ ' tables <+vars+> / missing;',
\ 'run;',
\ ],
\ 'macro': [
\ '%macro <+name+>(<+args+>);',
\ ' <+body+>',
\ '%mend <+name2+>;',
\ ],
\ 'header': [
\ '/*============================================================',
\ ' Program : <+program+>',
\ ' Purpose : <+purpose+>',
\ ' Author : <+author+>',
\ ' Date : <+date+>',
\ '============================================================*/',
\ ],
\ }
" 利用可能な種別(コマンド補完用)
function! saside#template#Kinds(...) abort
return join(sort(keys(s:templates)), "\n")
endfunction
" テンプレートをカーソル行の下に挿入する
function! saside#template#New(kind) abort
if !has_key(s:templates, a:kind)
call saside#util#Error('未知のテンプレート: ' . a:kind
\ . '(' . join(sort(keys(s:templates)), '/') . ')')
return v:false
endif
let lnum = line('.')
call append(lnum, s:templates[a:kind])
call cursor(lnum + 1, 1)
call saside#snippet#JumpNext()
return v:true
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/todo.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/todo.vim
" Module: todo — TODO/FIXME/BUG 一覧
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
function! saside#todo#List() abort
call saside#search#Grep('\C\<\%(TODO\|FIXME\|BUG\)\>')
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/saside/util.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: autoload/saside/util.vim
" Module: util — SASIDE 共通ユーティリティ
" Description: メッセージ表示・状態ディレクトリ・スクラッチバッファ生成など、
" 全モジュールが共有する最小限の道具箱。他モジュールに依存しない。
" Maintainer: SASIDE
" License: Vim License
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
" -----------------------------------------------------------------------------
" メッセージ表示(統一プレフィックス付き)
" -----------------------------------------------------------------------------
" 情報メッセージを表示する
function! saside#util#Info(msg) abort
echo '[saside] ' . a:msg
endfunction
" 警告を :messages に残る形で表示する
function! saside#util#Warn(msg) abort
echohl WarningMsg
echomsg '[saside] ' . a:msg
echohl None
endfunction
" エラーを :messages に残る形で表示する
function! saside#util#Error(msg) abort
echohl ErrorMsg
echomsg '[saside] ' . a:msg
echohl None
endfunction
" -----------------------------------------------------------------------------
" 状態ディレクトリ(セッション・ブックマーク等の永続化先)
" -----------------------------------------------------------------------------
" 状態ディレクトリの絶対パスを返す(無ければ作成する)
function! saside#util#StateDir() abort
let dir = expand(get(g:, 'saside_state_dir', '~/.vim/.state'))
if !isdirectory(dir)
call mkdir(dir, 'p', 0700)
endif
return dir
endfunction
" -----------------------------------------------------------------------------
" スクラッチバッファ
" -----------------------------------------------------------------------------
" saside://{role} という名前の使い捨てバッファを現在ウィンドウに用意する。
" 既存なら再利用する。バッファ番号を返す。
function! saside#util#Scratch(role) abort
let name = 'saside://' . a:role
let bufnr = bufnr(name)
if bufnr == -1
execute 'silent edit ' . fnameescape(name)
setlocal buftype=nofile bufhidden=hide noswapfile nobuflisted
setlocal nonumber norelativenumber nowrap
let bufnr = bufnr('%')
else
execute 'silent buffer ' . bufnr
endif
return bufnr
endfunction
" スクラッチバッファの内容を丸ごと置き換える(modifiable制御込み)
function! saside#util#SetLines(bufnr, lines) abort
call setbufvar(a:bufnr, '&modifiable', 1)
" deletebufline は Vim 8.1.0039+。無い環境では対象バッファで実行する
if exists('*deletebufline')
silent call deletebufline(a:bufnr, 1, '$')
call setbufline(a:bufnr, 1, a:lines)
else
let cur = bufnr('%')
execute 'silent buffer ' . a:bufnr
silent %delete _
call setline(1, a:lines)
execute 'silent buffer ' . cur
endif
call setbufvar(a:bufnr, '&modifiable', 0)
endfunction
" -----------------------------------------------------------------------------
" 汎用
" -----------------------------------------------------------------------------
" 文字列から前後空白を除く(Vim 8.0互換のtrim代替)
function! saside#util#Trim(s) abort
return substitute(a:s, '^\s*\|\s*$', '', 'g')
endfunction
" 機能の有無をまとめて調べる(run モジュールの実行方式判定用)
function! saside#util#Capabilities() abort
return {
\ 'job': has('job') && has('channel'),
\ 'terminal': has('terminal'),
\ 'timers': has('timers'),
\ }
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'autoload/sasomni.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/autoload/sasomni.vim
" SAS用オムニ補完エンジン(IntelliJのCtrl+Space相当)
"
" 補完対象:
" 1) SASキーワード(ステートメント/PROC名/関数/マクロ構文)
" 2) バッファ内で定義されたマクロ変数(%let, %macroの引数, call symput)
" -> "&" で始めると &var. 形式で補完
" 3) バッファ内で定義されたユーザーマクロ(%macro xxx)
" -> "%" で始めると %xxx 形式で補完
" 4) バッファ内に既出の識別子(データセット名・変数名など)
"
" ftplugin側で setlocal omnifunc=sasomni#Complete を設定して使う
" ============================================================
" 静的候補(メニューに種別を表示する)
" ============================================================
let s:statements = [
\ 'data','proc','run','quit','set','merge','update','modify','by',
\ 'where','output','input','infile','file','if','then','else','do',
\ 'end','until','while','select','when','otherwise','return','array',
\ 'retain','drop','keep','rename','length','format','informat','label',
\ 'title','footnote','options','libname','filename','call','put',
\ 'putlog','cards','datalines','attrib','ods'
\ ]
let s:procs = [
\ 'sql','means','summary','freq','print','report','sort','transpose',
\ 'datasets','contents','append','compare','corr','univariate','rank',
\ 'sgplot','gplot','tabulate','export','import','copy','reg','glm',
\ 'logistic','genmod','mixed','format','http','json'
\ ]
let s:functions = [
\ 'substr','scan','compress','upcase','lowcase','propcase','trim',
\ 'ltrim','strip','left','right','translate','tranwrd','index','find',
\ 'verify','input','put','sum','mean','std','n','nmiss','round','ceil',
\ 'floor','int','mod','abs','sqrt','exp','log','intck','intnx',
\ 'datepart','timepart','today','date','time','year','month','day',
\ 'mdy','hms','symget','symput','symputx','exist','open','close',
\ 'lag','dif','coalesce','coalescec','catx','cats','cat','length'
\ ]
let s:macrostmts = [
\ '%macro','%mend','%let','%put','%if','%then','%else','%do','%end',
\ '%to','%until','%while','%global','%local','%return','%abort',
\ '%include','%sysfunc','%sysevalf','%eval','%scan','%substr',
\ '%index','%length','%upcase','%lowcase','%str','%nrstr','%bquote',
\ '%nrbquote','%quote','%unquote','%superq','%symexist','%goto'
\ ]
let s:sqlkws = [
\ 'select','from','group by','having','order by','create table',
\ 'inner join','left join','right join','full join','on','as',
\ 'distinct','union','case','between','like','is null','exists',
\ 'count','avg','min','max','calculated','noprint','insert into'
\ ]
" ============================================================
" バッファスキャン
" ============================================================
" %let / %macro引数 / call symput で定義されたマクロ変数名を収集
function! s:ScanMacroVars() abort
let vars = {}
for line in getline(1, '$')
" %let name = ...
let m = matchlist(line, '\c%let\s\+\(\w\+\)')
if !empty(m)
let vars[m[1]] = 1
endif
" call symput('name', ...) / symputx
let m = matchlist(line, '\ccall\s\+symputx\?\s*(\s*[''"]\(\w\+\)[''"]')
if !empty(m)
let vars[m[1]] = 1
endif
" %macro name(p1=, p2, p3=x); の引数
let m = matchlist(line, '\c%macro\s\+\w\+\s*(\(.\{-}\)[);]')
if !empty(m)
for p in split(m[1], ',')
let pname = matchstr(p, '\w\+')
if pname !=# ''
let vars[pname] = 1
endif
endfor
endif
" select ... into :name (PROC SQL)
let m = matchlist(line, '\cinto\s*:\s*\(\w\+\)')
if !empty(m)
let vars[m[1]] = 1
endif
endfor
return keys(vars)
endfunction
" %macro で定義されたユーザーマクロ名を収集
function! s:ScanUserMacros() abort
let macros = {}
for line in getline(1, '$')
let m = matchlist(line, '\c%macro\s\+\(\w\+\)')
if !empty(m)
let macros[m[1]] = 1
endif
endfor
return keys(macros)
endfunction
" バッファ内の識別子(3文字以上)を収集
function! s:ScanBufferWords(prefix) abort
let words = {}
let cur_lnum = line('.')
for lnum in range(1, line('$'))
if lnum == cur_lnum
continue
endif
for w in split(getline(lnum), '\W\+')
if strlen(w) >= 3 && w =~# '^\a'
let words[w] = 1
endif
endfor
endfor
return keys(words)
endfunction
" ============================================================
" 本体 (omnifunc)
" ============================================================
function! sasomni#Complete(findstart, base) abort
if a:findstart
let line = getline('.')
let start = col('.') - 1
" & % も語の一部として遡る(&cutoff / %sysfunc の補完のため)
while start > 0 && line[start - 1] =~ '[[:alnum:]_&%]'
let start -= 1
endwhile
return start
endif
let base = a:base
let items = []
" ---- &xxx : マクロ変数補完 ----
if base =~# '^&'
let prefix = strpart(base, 1)
for v in sort(s:ScanMacroVars())
if prefix ==# '' || v =~? '^' . escape(prefix, '\')
call add(items, {'word': '&' . v . '.', 'abbr': '&' . v, 'menu': '[macro var]'})
endif
endfor
return items
endif
" ---- %xxx : マクロ構文・ユーザーマクロ補完 ----
if base =~# '^%'
for kw in s:macrostmts
if kw =~? '^' . escape(base, '\')
call add(items, {'word': kw, 'menu': '[macro stmt]'})
endif
endfor
for mname in sort(s:ScanUserMacros())
let cand = '%' . mname
if cand =~? '^' . escape(base, '\')
call add(items, {'word': cand, 'menu': '[user macro]'})
endif
endfor
return items
endif
" ---- 通常の識別子補完 ----
let pat = '^' . escape(base, '\')
for kw in s:statements
if kw =~? pat
call add(items, {'word': kw, 'menu': '[statement]'})
endif
endfor
for kw in s:procs
if kw =~? pat
call add(items, {'word': kw, 'menu': '[proc]'})
endif
endfor
for kw in s:functions
if kw =~? pat
call add(items, {'word': kw, 'menu': '[function]'})
endif
endfor
for kw in s:sqlkws
if kw =~? pat
call add(items, {'word': kw, 'menu': '[sql]'})
endif
endfor
" バッファ内既出語(重複は除く)
let seen = {}
for it in items
let seen[tolower(it.word)] = 1
endfor
for w in sort(s:ScanBufferWords(base))
if w =~? pat && !has_key(seen, tolower(w))
call add(items, {'word': w, 'menu': '[buffer]'})
endif
endfor
return items
endfunction
SASIDE_INSTALLER_EOF
write_file 'colors/sasdark.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/colors/sasdark.vim
" syntax_sas_ultimate.vim が使う全ハイライトグループに対応した専用カラースキーム。
" ランタイムの colors/ が使えない環境でも単体で完結する。
"
" グループ対応表(syntax/sas.vim側):
" Comment <- sasComment コメント
" String <- sasString 文字列
" Constant <- sasDateConst/HexConst/BitConst/Number 数値・日付・16進・2進定数
" Special <- sasFormat, sasAutoVar フォーマット表記・自動変数
" Identifier <- sasMacroVar マクロ変数 &var
" PreProc <- sasMacroCall, sasMacroDecl マクロ呼び出し・宣言(%macro %let)
" PreCondit <- sasMacroCond マクロ制御構文(%if %do %end)
" Function <- sasMacroFunc, sasFunction マクロ関数・データステップ関数
" Statement <- sasStatement, sasSqlKw, sasCallStmt 主要ステートメント/SQL/CALL
" Conditional<- sasConditional if/then/else/select/when
" Repeat <- sasRepeat do/end/until/while
" Operator <- sasOperator eq ne lt gt in and or 等
" Type <- sasProcName PROC名
hi clear
if exists("syntax_on")
syntax reset
endif
let g:colors_name = "sasdark"
set background=dark
" ---- 基本 ----
hi Normal ctermfg=252 ctermbg=234 guifg=#d0d0d0 guibg=#1c1c1c
" ---- コメント / 文字列 ----
hi Comment ctermfg=244 cterm=italic guifg=#888888 gui=italic
hi String ctermfg=150 guifg=#afd787
" ---- 数値・日付・16進・2進定数 ----
hi Constant ctermfg=180 guifg=#e8b589
hi Number ctermfg=180 guifg=#e8b589
" ---- フォーマット表記・自動変数(_N_ 等) ----
hi Special ctermfg=203 guifg=#ff5f5f
" ---- マクロ変数 &var ----
hi Identifier ctermfg=111 guifg=#87afff
" ---- マクロ呼び出し・宣言 (%macro %mend %let) ----
hi PreProc ctermfg=170 guifg=#d787d7 cterm=bold gui=bold
" ---- マクロ制御構文 (%if %then %do %end) ----
hi PreCondit ctermfg=141 guifg=#af87ff cterm=bold gui=bold
" ---- マクロ関数・データステップ関数 (%scan / substr / put 等) ----
hi Function ctermfg=79 guifg=#5fd7af
" ---- 主要ステートメント (data/proc/run/quit, SQL, call) ----
hi Statement ctermfg=214 guifg=#ffaf00 cterm=bold gui=bold
" ---- 制御構文 if/then/else/select/when/otherwise ----
hi Conditional ctermfg=208 guifg=#ff8700 cterm=bold gui=bold
" ---- 繰り返し do/end/until/while ----
hi Repeat ctermfg=178 guifg=#d7af00 cterm=bold gui=bold
" ---- 演算子 eq/ne/lt/gt/in/and/or/not ----
hi Operator ctermfg=182 guifg=#c9a6d6
" ---- PROC 名 ----
hi Type ctermfg=81 guifg=#5fd7ff
" ---- メッセージ類 ----
hi WarningMsg ctermfg=220 guifg=#ffd700 cterm=bold gui=bold
hi ErrorMsg ctermfg=196 guifg=#ff0000 cterm=bold gui=bold
" ---- UI ----
hi LineNr ctermfg=240 guifg=#585858
hi CursorLine ctermbg=236 guibg=#2e2e2e
hi Visual ctermbg=239 guibg=#4e4e4e
hi Pmenu ctermfg=252 ctermbg=236 guifg=#d0d0d0 guibg=#2e2e2e
hi PmenuSel ctermfg=234 ctermbg=214 guifg=#1c1c1c guibg=#ffaf00
SASIDE_INSTALLER_EOF
write_file 'dict/sas_formats.dict' <<'SASIDE_INSTALLER_EOF'
# formats / informats
best12. 数値の汎用表示
8. 幅8の数値
comma12. 3桁区切り
comma12.2 3桁区切り+小数2桁
dollar12.2 $付き3桁区切り
percent8.2 パーセント表示
z6. 前ゼロ埋め
yymmdd10. 2024-01-31形式
yymmddn8. 20240131形式
yymmdds10. 2024/01/31形式
date9. 31JAN2024形式
ddmmyy10. 31/01/2024形式
mmddyy10. 01/31/2024形式
datetime19. 日時表示
time8. hh:mm:ss
monyy7. JAN2024
year4. 年のみ
qtr1. 四半期
$char20. 文字(空白保持)
$20. 文字幅20
$upcase20. 大文字化して表示
$hex. 16進表示
SASIDE_INSTALLER_EOF
write_file 'dict/sas_functions.dict' <<'SASIDE_INSTALLER_EOF'
# SAS functions (word<TAB>description)
substr 部分文字列の抽出/置換 substr(s,pos<,len>)
scan 区切り文字で分割してn番目の語を返す
compress 指定文字を除去 compress(s<,chars,mod>)
compbl 連続空白を1つに圧縮
upcase 大文字化
lowcase 小文字化
propcase 先頭のみ大文字化
trim 末尾空白除去
strip 前後空白除去
left 左詰め
right 右詰め
length 文字列長(末尾空白除く)
lengthn 文字列長(空文字は0)
cat 文字列連結(空白保持)
cats 文字列連結(前後空白除去)
catx 区切り文字付き連結 catx(sep,...)
tranwrd 部分文字列の置換 tranwrd(s,from,to)
translate 文字単位の置換
index 部分文字列の位置
indexc 文字集合の位置
find 部分文字列検索(開始位置/修飾子指定可)
findc 文字集合検索
verify 集合外文字の位置
anyalpha 英字の位置
anydigit 数字の位置
anyspace 空白の位置
reverse 文字列反転
repeat 文字列の繰り返し
input 文字→数値/日付変換 input(s,informat.)
put 数値/日付→文字変換 put(x,format.)
inputn 実行時informat指定の変換
putn 実行時format指定の変換
sum 合計(欠損を無視)
mean 平均(欠損を無視)
median 中央値
std 標準偏差
var 分散
min 最小値
max 最大値
n 非欠損数
nmiss 欠損数
range 範囲(max-min)
round 丸め round(x<,unit>)
ceil 切り上げ
floor 切り捨て
int 整数部
mod 剰余
abs 絶対値
sign 符号
sqrt 平方根
exp 指数
log 自然対数
log2 底2の対数
log10 常用対数
today 今日の日付(SAS日付値)
date today と同じ
time 現在時刻
datetime 現在日時
year 日付→年
month 日付→月
day 日付→日
qtr 日付→四半期
week 日付→週番号
weekday 日付→曜日(1=日)
hour 時刻→時
minute 時刻→分
second 時刻→秒
mdy 年月日→SAS日付値 mdy(m,d,y)
ymd (mdy を使用)
hms 時分秒→SAS時刻値
dhms 日付+時分秒→日時値
datepart 日時値→日付部分
timepart 日時値→時刻部分
intck 期間の境界数 intck('unit',from,to)
intnx 期間の加算 intnx('unit',date,n<,align>)
datdif 日数差 datdif(d1,d2,basis)
yrdif 年数差 yrdif(d1,d2,basis)
lag 前行の値(キュー方式に注意)
dif 差分 x-lag(x)
coalesce 最初の非欠損値(数値)
coalescec 最初の非欠損値(文字)
missing 欠損なら1
ifn 条件演算(数値) ifn(cond,t,f<,m>)
ifc 条件演算(文字)
symget マクロ変数の値を取得
symput (callルーチン)マクロ変数へ格納
symputx (callルーチン)前後空白除去して格納
exist データセットの存在確認
open データセットを開く
close データセットを閉じる
attrn データセットの数値属性 attrn(id,'NOBS')
attrc データセットの文字属性
varnum 変数の位置番号
varname 位置番号→変数名
vartype 変数の型(N/C)
fetch 次の行を読む
fetchobs 指定行を読む
libref ライブラリ参照の確認
pathname librefの物理パス
resolve マクロ式を実行時解決
dequote 引用符除去
quote 引用符付与
rand 乱数 rand('uniform')等
ranuni 一様乱数(旧)
probnorm 標準正規分布関数
probit 正規分位関数
SASIDE_INSTALLER_EOF
write_file 'dict/sas_macros.dict' <<'SASIDE_INSTALLER_EOF'
# macro statements/functions
%macro マクロ定義開始
%mend マクロ定義終了
%let マクロ変数代入
%put ログ出力(%put _all_ で全変数)
%if 条件分岐(%then %do ... %end)
%then %ifの帰結
%else %ifの否定側
%do ブロック開始/反復(%do i=1 %to n)
%end ブロック終了
%to 反復の上限
%by 反復の刻み
%until 後判定ループ
%while 前判定ループ
%global グローバルマクロ変数宣言
%local ローカルマクロ変数宣言
%return マクロから抜ける
%abort 処理中断
%goto ラベルへ分岐
%include 外部ファイルの取り込み
%sysfunc データステップ関数をマクロで使う
%sysevalf 浮動小数の評価
%eval 整数の評価
%scan n番目の語
%qscan クォート版scan
%substr 部分文字列
%qsubstr クォート版substr
%index 位置検索
%length 文字列長
%upcase 大文字化
%qupcase クォート版upcase
%lowcase 小文字化(autocallマクロ)
%trim 末尾空白除去(autocall)
%left 先頭空白除去(autocall)
%cmpres 連続空白圧縮(autocall)
%str マクロクォート(; % 等を文字として扱う)
%nrstr & % も解決しないクォート
%bquote 実行時値のクォート
%nrbquote &も解決しないbquote
%quote bquoteの旧版
%unquote クォート解除
%superq 変数を解決せずに取得(最強のクォート)
%symexist マクロ変数の存在確認
%symglobl グローバルか判定
%symlocal ローカルか判定
%sysget 環境変数取得
SASIDE_INSTALLER_EOF
write_file 'dict/sas_ods.dict' <<'SASIDE_INSTALLER_EOF'
# ODS statements/destinations
listing リスト出力(既定の出力先)
html HTML出力
pdf PDF出力
rtf RTF(Word)出力
excel Excel出力(新しめの環境)
tagsets.excelxp Excel XML出力(9.3世代の定番)
csv CSV出力
output 出力オブジェクト→データセット化
trace 出力オブジェクト名の確認
select 出力オブジェクトの選択
exclude 出力オブジェクトの除外
graphics ODSグラフィックスon/off
close 出力先を閉じる
_all_ 全出力先(ods _all_ close)
style 出力スタイル指定 style=
file 出力ファイル指定 file=
path 出力パス指定
noproctitle プロシジャタイトル非表示
escapechar インラインフォーマット用エスケープ
SASIDE_INSTALLER_EOF
write_file 'dict/sas_options.dict' <<'SASIDE_INSTALLER_EOF'
# system options
mprint マクロ展開後のコードをログ出力
mlogic マクロ実行ロジックをログ出力
symbolgen マクロ変数解決をログ出力
merror 未定義マクロ呼び出しを警告
nomprint mprint無効化
obs= 処理する最大オブザベーション数
firstobs= 処理開始オブザベーション
nodate 出力に日付を出さない
date 出力に日付を出す
nonumber ページ番号を出さない
number ページ番号を出す
nocenter 左詰め出力
center 中央寄せ出力
linesize= 1行の幅 (ls=)
pagesize= 1ページの行数 (ps=)
missing= 欠損値の表示文字
fmterr 未定義フォーマットをエラーに
nofmterr 未定義フォーマットを警告に留める
errors= 同一エラーの最大表示回数
source ソースをログに出す
nosource ソースをログに出さない
source2 include先ソースもログに出す
notes NOTEを出す
nonotes NOTEを抑止(デバッグ後は戻すこと)
fullstimer 詳細なリソース使用量をログ出力
stimer 簡易リソース表示
compress= データセット圧縮 (yes/binary)
reuse= 圧縮領域の再利用
work= WORKライブラリの場所
sortsize= ソートに使うメモリ
memsize= 最大メモリ
validvarname= 変数名規則 (v7/upcase/any)
yearcutoff= 2桁年の解釈基準年
SASIDE_INSTALLER_EOF
write_file 'dict/sas_procs.dict' <<'SASIDE_INSTALLER_EOF'
# SAS procedures
sql SQL処理(結合・集計・ビュー)
sort 並べ替え(by/nodupkey/out=)
means 基本統計量(n mean std min max)
summary meansのprint無し版
freq 度数集計・クロス表・カイ二乗
univariate 分布の詳細統計・パーセンタイル
corr 相関係数
print データセットの一覧出力
report 体裁付きレポート
tabulate 多次元集計表
transpose 行列転置(by/id/var)
datasets データセット管理(delete/copy/modify)
contents データセットの構造表示
append データセットの追記
compare データセットの比較
rank 順位付け
standard 標準化(旧)
stdize 標準化
format ユーザー定義フォーマット作成
export 外部ファイルへ出力(csv等)
import 外部ファイル取込
copy ライブラリ間コピー
reg 線形回帰
glm 一般線形モデル
logistic ロジスティック回帰
genmod 一般化線形モデル
mixed 混合モデル
phreg Cox比例ハザード
lifetest 生存時間解析
factor 因子分析
cluster 階層クラスタ分析
fastclus k-meansクラスタ
arima 時系列ARIMA
timeseries 時系列集計
gplot 散布図/折れ線(旧グラフ)
gchart 棒/円グラフ(旧グラフ)
sgplot 統計グラフ(新)
sgpanel パネルグラフ
boxplot 箱ひげ図
catalog カタログ管理
options オプション一覧表示
printto ログ/出力の出力先変更
fcmp ユーザー定義関数作成
SASIDE_INSTALLER_EOF
write_file 'dict/snippets.txt' <<'SASIDE_INSTALLER_EOF'
# SASIDE snippets (snippet key description / body until next 'snippet')
snippet datastep DATAステップ骨格
data <+out+>;
set <+in+>;
<+statements+>
run;
snippet sql PROC SQL骨格
proc sql;
create table <+out+> as
select <+columns+>
from <+in+>;
quit;
snippet sortds PROC SORT
proc sort data=<+in+> out=<+out+>;
by <+keys+>;
run;
snippet meansds PROC MEANS
proc means data=<+in+> n mean std min max maxdec=2;
var <+vars+>;
run;
snippet freqds PROC FREQ
proc freq data=<+in+>;
tables <+vars+>;
run;
snippet macrodef マクロ定義
%macro <+name+>(<+args+>);
<+body+>
%mend;
snippet doloop DOループ
do <+i+> = 1 to <+n+>;
<+body+>
end;
snippet selwhen SELECT-WHEN
select (<+var+>);
when (<+v1+>) <+action1+>;
otherwise <+action2+>;
end;
snippet importcsv CSV取込
proc import datafile="<+path+>" out=<+out+> dbms=csv replace;
getnames=yes;
run;
snippet exportcsv CSV出力
proc export data=<+in+> outfile="<+path+>" dbms=csv replace;
run;
SASIDE_INSTALLER_EOF
write_file 'doc/saside.txt' <<'SASIDE_INSTALLER_EOF'
*saside.txt* Pure Vim Script SAS IDE *saside*
SASIDE — SAS 9.3 向け Vim IDE(外部依存ゼロ)
==============================================================================
目次 *saside-contents*
1. 概要 ................................ |saside-intro|
2. インストール ........................ |saside-install|
3. コマンド ............................ |saside-commands|
4. キーマップ .......................... |saside-mappings|
5. オプション .......................... |saside-options|
6. 既知の限界 .......................... |saside-limitations|
==============================================================================
1. 概要 *saside-intro*
SASIDE は Linux 上の SAS 9.3 バッチ開発を Vim だけで完結させる IDE です。
純粋な Vim Script のみで実装され、Node.js / Python / LSP / ネットワーク
アクセスを一切必要としません。~/.vim 配下へのコピーだけで動作します。
中核は「コードインデックス」です。編集中の SAS コードを1パスで解析し、
DATA/PROC/%MACRO/データセット/マクロ変数を抽出します。Navigator・Outline・
Dataset Browser・Macro Browser・ステータスライン・補完のすべてが、この
単一のインデックスを共有します。
==============================================================================
2. インストール *saside-install*
WinSCP 等で本一式を ~/.vim/ へコピーし、Vim を再起動してください。
ヘルプを有効にするには一度だけ実行します: >
:helptags ~/.vim/doc
<
会社の実行運用(`sas program.txt`)に合わせる場合は vimrc に: >
let g:saside_sas_cmd = 'sas' " フルパス指定も可
<
==============================================================================
3. コマンド *saside-commands*
実行・ログ ~
:SasRun [file] 保存して SAS をバッチ実行(既定 F5)
:{range}SasRunSelection 選択範囲だけを実行(ビジュアルモードで F5)
:SasRunBlock カーソル位置のDATA/PROCブロックだけ実行 (\rb)
:SasCheck obs=0 構文チェック実行(データ処理せず検証)(\rc)
:SasRunTerm [file] +terminal 環境では端末内で実行
:SasAbort 実行中ジョブの停止
:SasLog [file] ログを下ペインに表示(F6 でトグル)
:SasLst [file] .lst を下ペインに表示 (F7)
:SasQuickfix ERROR+WARNING を quickfix へ (F8)
:SasLogErrors ERROR のみ抽出
:SasLogWarnings WARNING のみ抽出
:SasLogNotes NOTE のみ抽出
:SasLogRisk [file] 危険NOTE(黙って誤結果を出す類)を抽出 (\lr)
ビュー ~
:SasExplorer 左: ファイルツリー (F2)
:SasNav 右: Navigator (F3)
:SasOutline 右: Outline (F4)
:SasDatasets 右: Dataset Browser
:SasMacros 右: Macro Browser
:SasIde 一括 IDE レイアウト (F12)
編集支援 ~
:SasFormat 再インデント+空白整理(F9。
g:saside_format_upcase=1 で予約語大文字化)
:SasNew {kind} テンプレート挿入 (data/sql/sort/means/freq/
macro/header)
:SasHelp [word] 辞書ヘルプ(SASバッファでは K でも可)
検索 ~
:SasGrep {pat} プロジェクト横断検索 (\sg)
:SasFindDs [name] データセット使用箇所検索 (\sd)
:SasFindMacro [name] マクロ定義/呼び出し検索 (\sm)
:SasTodo TODO/FIXME/BUG 一覧 (F10)
その他 ~
:SasBookmark [name] 現在行をブックマーク (\bb)
:SasBookmarks ブックマーク一覧 (\bl)
:SasBookmarkClear 全削除
:SasSessionSave [n] セッション保存
:SasSessionLoad [n] セッション復元
:SasBlockInfo カーソル行のブロック情報
:SasIndex コードインデックスのダンプ(デバッグ用)
==============================================================================
4. キーマップ *saside-mappings*
既定割当(g:saside_no_default_maps=1 で無効化、<Plug> で個別再割当可):
F2 Explorer F3 Navigator F4 Outline
F5 SAS実行 F6 ログトグル F7 .lst表示
{Visual}F5 選択範囲だけ実行 \rb カーソル位置のブロックだけ実行
F8 Quickfix F9 整形 F10 TODO一覧
F12 IDEレイアウト
<C-j> (挿入) スニペット展開 / 次プレースホルダ
K (SAS) 辞書ヘルプ
\rc 構文チェック(obs=0) \lr 危険NOTE抽出
\sg \sd \sm 検索系 \bb \bl ブックマーク
Navigator 内: Enter=ジャンプ q=閉じる r=更新 1/2/3/4=モード切替
Explorer 内: Enter/o=開く a=新規 A=新規Dir D=削除 r=改名 m=移動
c=コピー u=親へ C=ルート変更 R=更新 q=閉じる
==============================================================================
5. オプション *saside-options*
g:saside_sas_cmd SAS実行コマンド(既定 'sas')
g:saside_sas_args 追加引数リスト(既定 [])
g:saside_run_preamble 部分実行時に前方のLIBNAME/OPTIONS/%LETを
自動で先頭に付ける(既定 1)
g:saside_check_args :SasCheck の引数
(既定 ['-obs','0','-noreplace'])
g:saside_log_risk_patterns 危険NOTEパターンの追加
[{'pat':正規表現,'desc':説明}, ...]
g:saside_txt_detect *.txt のSAS自動判定(既定 1)
g:saside_txt_threshold 判定に必要なSAS特徴行数(既定 3)
g:saside_index_maxlines リアルタイム解析の上限行数(既定 20000)
g:saside_debounce_ms 解析デバウンス(既定 300)
g:saside_explorer_width 左ペイン幅(既定 28)
g:saside_navigator_width 右ペイン幅(既定 34)
g:saside_log_height 下ペイン高(既定 10)
g:saside_nav_stay ジャンプ後もNavigatorに留まる(既定 0)
g:saside_format_upcase 整形時に予約語を大文字化(既定 0)
g:saside_session_auto 終了時に自動セッション保存(既定 0)
g:saside_project_root プロジェクトルート固定(既定 自動検出。
.sasproject ファイルでもマーク可)
g:saside_state_dir 状態保存先(既定 ~/.vim/.state)
g:saside_no_default_maps 既定キー割当を無効化(既定 0)
==============================================================================
6. 既知の限界 *saside-limitations*
- パーサは正規表現ベースであり、完全な構文解析ではありません。
マクロが生成するコード(%include・コード生成マクロ)の中身は
追跡できません。
- ログの自動追尾は実行完了時のみ更新されます(実行中のtailはしません)。
- 意味解析を要するリファクタリング(変数の安全な一括リネーム等)は
対象外です。:SasGrep + cdo での一括置換を推奨します。
vim:tw=78:ts=8:ft=help:norl:
SASIDE_INSTALLER_EOF
write_file 'filetype.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/filetype.vim
" $VIMRUNTIME/filetype.vim が壊れている/存在しない環境向けの補完ブートストラップ。
"
" 重要: 自分以外の runtimepath 上に filetype.vim が存在するなら、
" そちらに処理を委譲して何もしない($VIMRUNTIME という環境変数の有無に
" 依存させず、実際の 'runtimepath' を globpath() で直接調べる)。
" これにより、システム側が生きている環境ではシステム版の大量の言語判定を
" 壊さず、SASのftdetectだけが素直に additive に効く。
if exists("did_load_filetypes")
finish
endif
let s:self = fnamemodify(expand('<sfile>:p'), ':p')
let s:other_exists = 0
for s:f in globpath(&runtimepath, 'filetype.vim', 0, 1)
if fnamemodify(s:f, ':p') !=# s:self
let s:other_exists = 1
break
endif
endfor
unlet! s:self s:f
if s:other_exists
unlet s:other_exists
finish
endif
unlet s:other_exists
let did_load_filetypes = 1
augroup filetypedetect
au!
" 全 runtimepath 上の ftdetect/*.vim を読み込む
runtime! ftdetect/*.vim
augroup END
SASIDE_INSTALLER_EOF
write_file 'ftdetect/sas.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: ftdetect/sas.vim
" Module: SASIDE ファイルタイプ判定
" Description: *.sas は無条件、*.txt は内容ベースのスコア判定で filetype=sas
" を設定する(会社運用が `sas program.txt` のため)。
" *.log / *.lst は内容から saslog / saslst を割り当てる。
" Options: g:saside_txt_detect (既定1) .txt判定の有効/無効
" g:saside_txt_threshold (既定3) SAS特徴行の必要数
" =============================================================================
au BufNewFile,BufRead *.sas setfiletype sas
" --- .txt: 内容スコアリングによる判定 ---
" 先頭200行に DATA/PROC/%MACRO/LIBNAME/OPTIONS/%LET の行が閾値以上あればSAS
function! s:DetectSasTxt() abort
if !get(g:, 'saside_txt_detect', 1)
return
endif
let score = 0
let threshold = get(g:, 'saside_txt_threshold', 3)
for line in getline(1, 200)
if line =~? '^\s*\%(data\|proc\|libname\|options\|%macro\|%let\|run\s*;\|quit\s*;\)'
let score += 1
if score >= threshold
setfiletype sas
return
endif
endif
endfor
endfunction
au BufNewFile,BufRead *.txt call s:DetectSasTxt()
" --- .log / .lst: SAS出力の判定 ---
function! s:DetectSasLog() abort
if getline(1) =~# 'The SAS System'
\ || search('^\d\+\s*NOTE:', 'nw')
\ || search('^\d\+\s*ERROR:', 'nw')
setfiletype saslog
endif
endfunction
au BufNewFile,BufRead *.log call s:DetectSasLog()
function! s:DetectSasLst() abort
if getline(1) =~# 'The SAS System'
setfiletype saslst
endif
endfunction
au BufNewFile,BufRead *.lst call s:DetectSasLst()
SASIDE_INSTALLER_EOF
write_file 'ftplugin.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/ftplugin.vim
" $VIMRUNTIME/ftplugin.vim の代替ブートストラップ。
" FileType イベント発生時に ftplugin/<filetype>.vim を読み込む「配線」を行う。
" 自分以外の runtimepath 上に ftplugin.vim があればそちらに委譲する。
if exists("did_load_ftplugin")
finish
endif
let s:self = fnamemodify(expand('<sfile>:p'), ':p')
let s:other_exists = 0
for s:f in globpath(&runtimepath, 'ftplugin.vim', 0, 1)
if fnamemodify(s:f, ':p') !=# s:self
let s:other_exists = 1
break
endif
endfor
unlet! s:self s:f
if s:other_exists
unlet s:other_exists
finish
endif
unlet s:other_exists
let did_load_ftplugin = 1
augroup filetypeplugin
au!
au FileType * call s:LoadFTPlugin()
augroup END
function! s:LoadFTPlugin() abort
if exists("b:undo_ftplugin")
silent! exe b:undo_ftplugin
unlet! b:undo_ftplugin b:did_ftplugin
endif
let s = expand("<amatch>")
if s !=# ""
let name = substitute(s, '\.', '_', 'g')
exe 'runtime! ftplugin/' . name . '.vim ftplugin/' . name . '_*.vim ftplugin/' . name . '/*.vim'
endif
endfunction
SASIDE_INSTALLER_EOF
write_file 'ftplugin/sas.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/ftplugin/sas.vim
if exists("b:did_ftplugin")
finish
endif
let b:did_ftplugin = 1
let s:save_cpo = &cpo
set cpo&vim
setlocal commentstring=/*\ %s\ */
setlocal comments=s1:/*,mb:*,ex:*/,:%*
setlocal expandtab
setlocal shiftwidth=4
setlocal tabstop=4
setlocal softtabstop=4
setlocal autoindent
setlocal iskeyword+=%,&
setlocal formatoptions-=t
" IntelliJ風補完(plugin/ide.vim のスマートTab / Ctrl+Space が利用する)
setlocal omnifunc=sasomni#Complete
" --- 折り畳み: DATA/PROC ~ RUN;/QUIT; を1ブロックとして畳む ---
setlocal foldmethod=expr
setlocal foldexpr=SasFoldExpr(v:lnum)
setlocal foldtext=SasFoldText()
function! SasFoldExpr(lnum) abort
let line = getline(a:lnum)
if line =~? '^\s*\(data\|proc\)\>'
return '>1'
elseif line =~? '^\s*\(run\|quit\)\s*;'
return '<1'
endif
return '='
endfunction
function! SasFoldText() abort
return getline(v:foldstart) . ' ... (' . (v:foldend - v:foldstart + 1) . ' lines)'
endfunction
" --- 頻出スニペットの略語 ---
iabbrev <buffer> pdsql proc sql;<CR>quit;<Esc>O
iabbrev <buffer> ppmeans proc means data=;<CR>run;<Esc>O
iabbrev <buffer> ddstep data ;<CR>set ;<CR>run;<Esc>kkA
" --- 実行/ログ確認/整形マッピング(autoload/sas.vim に処理を委譲) ---
nnoremap <buffer> <F5> :call sas#run(expand('%:p'))<CR>
nnoremap <buffer> <F6> :call sas#openlog(expand('%:p:r') . '.log')<CR>
nnoremap <buffer> <F7> :call sas#openlst(expand('%:p:r') . '.lst')<CR>
nnoremap <buffer> <F8> :call sas#quickfixlog(expand('%:p:r') . '.log')<CR>
nnoremap <buffer> <F9> :call sas#format()<CR>
command! -buffer SasFormat call sas#format()
" --- ブロック間ジャンプ: ]] で次の DATA/PROC/%MACRO へ、[[ で前へ ---
nnoremap <buffer> <silent> ]] :call search('\c^\s*\%(data\<Bar>proc\<Bar>%macro\)\>', 'W')<CR>
nnoremap <buffer> <silent> [[ :call search('\c^\s*\%(data\<Bar>proc\<Bar>%macro\)\>', 'bW')<CR>
" --- スマート % : DO/END, %DO/%END, %MACRO/%MEND, DATA・PROC/RUN の対応へ
" ジャンプ(対象外の位置では通常の % として振る舞う) ---
nnoremap <buffer> <silent> % :call sas#matchjump()<CR>
let b:undo_ftplugin = "setlocal cms< com< et< sw< ts< sts< ai< iskeyword< fo< fdm< fde< fdt< omnifunc<"
\ . " | silent! iunmap <buffer> pdsql | silent! iunmap <buffer> ppmeans | silent! iunmap <buffer> ddstep"
\ . " | silent! nunmap <buffer> <F5> | silent! nunmap <buffer> <F6>"
\ . " | silent! nunmap <buffer> <F7> | silent! nunmap <buffer> <F8>"
\ . " | silent! nunmap <buffer> <F9> | silent! delcommand SasFormat"
\ . " | silent! nunmap <buffer> ]] | silent! nunmap <buffer> [["
\ . " | silent! nunmap <buffer> %"
let &cpo = s:save_cpo
unlet s:save_cpo
SASIDE_INSTALLER_EOF
write_file 'ftplugin/saslog.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: ftplugin/saslog.vim
" Module: SASログバッファのローカル設定
" Keymaps: ]e/[e 次/前のERROR ]w/[w 次/前のWARNING q 閉じる
" =============================================================================
if exists('b:did_ftplugin')
finish
endif
let b:did_ftplugin = 1
setlocal autoread nowrap nomodifiable readonly
setlocal nonumber
nnoremap <buffer> <silent> ]e :call search('^\d*\s*ERROR', 'W')<CR>
nnoremap <buffer> <silent> [e :call search('^\d*\s*ERROR', 'bW')<CR>
nnoremap <buffer> <silent> ]w :call search('^\d*\s*WARNING', 'W')<CR>
nnoremap <buffer> <silent> [w :call search('^\d*\s*WARNING', 'bW')<CR>
nnoremap <buffer> <silent> q :close<CR>
let b:undo_ftplugin = 'setlocal ar< wrap< ma< ro< nu<'
\ . ' | silent! nunmap <buffer> ]e | silent! nunmap <buffer> [e'
\ . ' | silent! nunmap <buffer> ]w | silent! nunmap <buffer> [w'
\ . ' | silent! nunmap <buffer> q'
SASIDE_INSTALLER_EOF
write_file 'indent.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/indent.vim
" $VIMRUNTIME/indent.vim の代替ブートストラップ。
" 自分以外の runtimepath 上に indent.vim があればそちらに委譲する。
if exists("did_indent_on")
finish
endif
let s:self = fnamemodify(expand('<sfile>:p'), ':p')
let s:other_exists = 0
for s:f in globpath(&runtimepath, 'indent.vim', 0, 1)
if fnamemodify(s:f, ':p') !=# s:self
let s:other_exists = 1
break
endif
endfor
unlet! s:self s:f
if s:other_exists
unlet s:other_exists
finish
endif
unlet s:other_exists
let did_indent_on = 1
augroup filetypeindent
au!
au FileType * call s:LoadIndent()
augroup END
function! s:LoadIndent() abort
if exists("b:undo_indent")
silent! exe b:undo_indent
unlet! b:undo_indent b:did_indent
endif
let s = expand("<amatch>")
if s !=# ""
let name = substitute(s, '\.', '_', 'g')
exe 'runtime! indent/' . name . '.vim'
endif
endfunction
SASIDE_INSTALLER_EOF
write_file 'indent/sas.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/indent/sas.vim
" SAS用スマートインデント(DATA/PROC-RUN、DO/SELECT-END、%DO-%END、
" %MACRO-%MEND のネストを searchpair() で正しく追跡し、複数行にまたがる
" 文(継続行)も検出して字下げする)
if exists("b:did_indent")
finish
endif
let b:did_indent = 1
setlocal indentexpr=SasIndent()
setlocal indentkeys=0{,0},0),!^F,o,O,=~end,=~run,=~quit,=~%mend,=~%end,=~else
let s:cpo_save = &cpo
set cpo&vim
" ============================================================
" ヘルパー
" ============================================================
" 指定位置がコメント/文字列の中かどうか
function! s:InComment(lnum, col) abort
let name = synIDattr(synID(a:lnum, a:col, 1), "name")
return name =~? 'sasComment\|sasString'
endfunction
" searchpair() 用: マッチ位置がコメント/文字列内なら真(スキップ対象)
function! s:SkipCommentExpr() abort
return s:InComment(line('.'), col('.'))
endfunction
" コメント部分を取り除いた行テキストを返す
" 同一行内で閉じている /* ... */ と行頭の * ... コメントに加え、
" この行の途中で開いたまま閉じていない /* コメント(次行以降へ継続)も
" 末尾から取り除く
function! s:StripTrailingComment(line) abort
let l = a:line
let l = substitute(l, '/\*.\{-}\*/', '', 'g')
let l = substitute(l, '^\(\s*\)\*.*', '\1', '')
let l = substitute(l, '/\*.*$', '', '')
return l
endfunction
" 行の最初の非空白文字が、複数行コメント/文字列の「継続行」の中に
" あるかどうか(=この行にコードが一切無く、前の行から続くコメントの
" 内部にいる状態)を syntax 情報から判定する
function! s:IsPureCommentContinuation(lnum) abort
let line = getline(a:lnum)
let firstcol = match(line, '\S') + 1
if firstcol <= 0
return 0
endif
" この行自体がコメントを開始している(/* または行頭の *)なら、
" それは「開始行」であって継続行ではない。通常のインデント計算に委ねる
if line[firstcol - 1:] =~ '^/\*\|^\*'
return 0
endif
return s:InComment(a:lnum, firstcol)
endfunction
" 空行・コメント継続行・「中身がコメントだけの行」を読み飛ばして
" 直前の実コードが残っている行を返す
" (例: "/* これは" のように開き括弧だけの行は、この行自体には
" 実コードが無いので prev-code とはみなさず、さらに前へ遡る)
function! s:PrevCodeLine(lnum) abort
let l = prevnonblank(a:lnum - 1)
while l > 0
if s:IsPureCommentContinuation(l) || s:StripTrailingComment(getline(l)) =~ '^\s*$'
let l = prevnonblank(l - 1)
continue
endif
return l
endwhile
return 0
endfunction
" 行が「;」で終わっている(文が完結している)か
function! s:IsTerminated(line) abort
return s:StripTrailingComment(a:line) =~ ';\s*$'
endfunction
" 現在の(継続中かもしれない)文が実際に始まった行番号を探す
function! s:StatementStartLine(lnum) abort
let l = a:lnum
while l > 1
let p = s:PrevCodeLine(l)
if p == 0
return l
endif
if s:IsTerminated(getline(p))
return l
endif
let l = p
endwhile
return l
endfunction
" ============================================================
" ブロック開始/終了パターン
" ============================================================
let s:pat_data_proc_open = '\c^\s*\%(data\|proc\)\>'
let s:pat_run_quit_close = '\c^\s*\%(run\|quit\)\s*;'
let s:pat_do_select_open = '\c\<\%(do\|select\)\>.\{-};\s*$'
let s:pat_end_close = '\c^\s*end\s*;'
let s:pat_pctdo_open = '\c%do\>.\{-};\s*$'
let s:pat_pctend_close = '\c^\s*%end\s*;'
let s:pat_macro_open = '\c^\s*%macro\>'
let s:pat_mend_close = '\c^\s*%mend\>'
" ============================================================
" 本体
" ============================================================
function! SasIndent() abort
let lnum = v:lnum
" この行が複数行コメント(/* ... */)の継続行であれば、
" ユーザーが手で整えた体裁を尊重してインデントを変更しない
if s:IsPureCommentContinuation(lnum)
return -1
endif
let pnum = s:PrevCodeLine(lnum)
if pnum == 0
return 0
endif
let cline_raw = getline(lnum)
let cline = s:StripTrailingComment(cline_raw)
let pline = s:StripTrailingComment(getline(pnum))
" ---------------------------------------------------
" 0) 括弧内ルール(データセットオプションの複数行記述など)
" 現在行が閉じていない '(' の内側にある場合:
" ・行頭が ')' → 開き括弧のある行に揃える
" ・「名前 =」形式(keep=等) → 開き括弧の行 +1段
" ・それ以外(値の列挙) → 開き括弧の行 +2段
" 例:
" set test&&yymm&i( ← 開き括弧行 (8)
" keep = ← +1段 (12)
" aaa ← +2段 (16)
" rename = ( ← +1段 (12) ここが新たな開き括弧行
" aaa = a ← rename行の+1段 (16)
" ) ← rename行に揃う (12)
" ); ← set行に揃う (8)
" ---------------------------------------------------
call cursor(lnum, 1)
let open = searchpairpos('(', '', ')', 'bnW',
\ 's:SkipCommentExpr()', max([1, lnum - 500]))
if open[0] > 0
if cline =~ '^\s*)'
return indent(open[0])
elseif cline =~ '^\s*[A-Za-z_&%][A-Za-z0-9_&.%]*\s*='
return indent(open[0]) + shiftwidth()
else
return indent(open[0]) + 2 * shiftwidth()
endif
endif
" ---------------------------------------------------
" 1) 現在行がブロックの「閉じ」であれば、対応する開始行を
" searchpair() で探してその行のインデントに合わせる
" ---------------------------------------------------
if cline =~ s:pat_run_quit_close
call cursor(lnum, 1)
let m = searchpair(s:pat_data_proc_open, '', s:pat_run_quit_close,
\ 'bnW', 's:SkipCommentExpr()')
return m > 0 ? indent(m) : 0
endif
if cline =~ s:pat_pctend_close
call cursor(lnum, 1)
let m = searchpair(s:pat_pctdo_open, '', s:pat_pctend_close,
\ 'bnW', 's:SkipCommentExpr()')
return m > 0 ? indent(m) : indent(pnum)
endif
if cline =~ s:pat_mend_close
call cursor(lnum, 1)
let m = searchpair(s:pat_macro_open, '', s:pat_mend_close,
\ 'bnW', 's:SkipCommentExpr()')
return m > 0 ? indent(m) : 0
endif
if cline =~ s:pat_end_close
call cursor(lnum, 1)
let m = searchpair(s:pat_do_select_open, '', s:pat_end_close,
\ 'bnW', 's:SkipCommentExpr()')
return m > 0 ? indent(m) : indent(pnum)
endif
" ---------------------------------------------------
" 2) ELSE は対応する IF/THEN と同じ深さに揃える
" (直前がEND;等で既に正しい深さに戻っているのでそのまま踏襲)
" ---------------------------------------------------
if cline =~ '\c^\s*else\>'
return indent(pnum)
endif
" ---------------------------------------------------
" 3) 直前行が文を終端していない(継続行)場合は、文の開始行を
" 見つけてそこから1段深くする
" ---------------------------------------------------
if !s:IsTerminated(pline)
let stmt_start = s:StatementStartLine(pnum)
return indent(stmt_start) + shiftwidth()
endif
" ---------------------------------------------------
" 4) 直前行が新しいブロックを開いているなら1段深くする
" ---------------------------------------------------
let ind = indent(pnum)
if pline =~ s:pat_data_proc_open
let ind += shiftwidth()
elseif pline =~ s:pat_macro_open
let ind += shiftwidth()
elseif pline =~ s:pat_pctdo_open
let ind += shiftwidth()
elseif pline =~ s:pat_do_select_open
let ind += shiftwidth()
endif
return ind
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
let b:undo_indent = "setlocal indentexpr< indentkeys<"
SASIDE_INSTALLER_EOF
write_file 'plugin/autopairs.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/plugin/autopairs.vim
" IntelliJ / VSCode 風の括弧・クオート自動補完(全filetype共通)
"
" ( [ { " ' を入力 -> 対応する閉じ側を自動挿入しカーソルを間に置く
" 閉じ文字がすでに直後にある -> 重複挿入せず、その上を素通りする
" 空のペア (() [] {} "" '') の内側で <BS> -> 両方を同時に削除
" 空のペア (() {} []) の内側で <CR> -> インデント付きで展開する
"
" 無効化したい場合は vimrc 等で下記のいずれかを設定する
" let g:autopairs_enabled = 0 全体を無効化
" let b:autopairs_enabled = 0 特定バッファのみ無効化
" コマンド :AutoPairsToggle でも切り替えられる
if exists("g:loaded_autopairs")
finish
endif
let g:loaded_autopairs = 1
if !exists("g:autopairs_enabled")
let g:autopairs_enabled = 1
endif
let s:pairs = {'(': ')', '[': ']', '{': '}'}
function! s:Enabled() abort
if exists('b:autopairs_enabled') && !b:autopairs_enabled
return 0
endif
return g:autopairs_enabled
endfunction
function! s:CharBefore() abort
let l = getline('.')
let c = col('.')
return c > 1 ? l[c - 2] : ''
endfunction
function! s:CharAfter() abort
let l = getline('.')
return l[col('.') - 1]
endfunction
" ---- 開き括弧 ----
function! s:PairOpen(open) abort
if !s:Enabled()
return a:open
endif
return a:open . s:pairs[a:open] . "\<Left>"
endfunction
" ---- 閉じ括弧(直後に同じ閉じ文字があれば素通り) ----
function! s:PairClose(close) abort
if s:Enabled() && s:CharAfter() ==# a:close
return "\<Right>"
endif
return a:close
endfunction
" ---- クオート ----
function! s:PairQuote(q) abort
if !s:Enabled()
return a:q
endif
if s:CharAfter() ==# a:q
return "\<Right>"
endif
" 英数字・アンダースコア・バックスラッシュの直後は補完しない
" (既存の単語の途中でクオートを打つケースを誤爆させないため)
if s:CharBefore() =~# '[[:alnum:]_\\]'
return a:q
endif
return a:q . a:q . "\<Left>"
endfunction
" ---- Backspace: 空のペアの内側なら両方消す ----
function! s:PairBS() abort
if s:Enabled()
let before = s:CharBefore()
let after = s:CharAfter()
let closers = {'(': ')', '[': ']', '{': '}', '"': '"', "'": "'"}
if has_key(closers, before) && after ==# closers[before]
return "\<BS>\<Del>"
endif
endif
return "\<BS>"
endfunction
" ---- Enter: 空の ()/[]/{} の内側ならインデント付きで展開 ----
function! s:PairCR() abort
if pumvisible()
" 補完ポップアップ表示中は候補を確定する(IntelliJのEnter確定と同じ)
return "\<C-y>"
endif
if s:Enabled()
let before = s:CharBefore()
let after = s:CharAfter()
if has_key(s:pairs, before) && after ==# s:pairs[before]
return "\<CR>\<Esc>O"
endif
endif
return "\<CR>"
endfunction
inoremap <expr> ( <SID>PairOpen('(')
inoremap <expr> [ <SID>PairOpen('[')
inoremap <expr> { <SID>PairOpen('{')
inoremap <expr> ) <SID>PairClose(')')
inoremap <expr> ] <SID>PairClose(']')
inoremap <expr> } <SID>PairClose('}')
inoremap <expr> " <SID>PairQuote('"')
inoremap <expr> ' <SID>PairQuote("'")
inoremap <expr> <BS> <SID>PairBS()
inoremap <expr> <CR> <SID>PairCR()
command! AutoPairsToggle let g:autopairs_enabled = !g:autopairs_enabled
\ | echo "autopairs: " . (g:autopairs_enabled ? "ON" : "OFF")
SASIDE_INSTALLER_EOF
write_file 'plugin/ide.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/plugin/ide.vim
" IntelliJ風の操作感を提供するグローバル設定・キーマップ
"
" Tab : 単語の途中なら補完ポップアップを開く / ポップアップ中は次候補
" Shift+Tab : ポップアップ中は前候補
" Ctrl+Space : 補完を明示的に起動(端末では <C-@> として届く)
" Enter : ポップアップ中は候補を確定(autopairs.vim側で処理)
" Ctrl+/ : 行コメントのトグル(端末では <C-_> として届く)ノーマル/ビジュアル
" Alt+j/k : 行を下/上に移動(IntelliJのShift+Alt+Up/Down相当)
" Tab/S-Tab : (ビジュアルモード)インデント/逆インデント。選択を維持
"
" 無効化: let g:ide_keymaps_enabled = 0 を vimrc に書く
if exists("g:loaded_ide_keymaps")
finish
endif
let g:loaded_ide_keymaps = 1
if exists("g:ide_keymaps_enabled") && !g:ide_keymaps_enabled
finish
endif
" ============================================================
" 補完まわりの体感調整
" ============================================================
" menuone のみ(longest や noselect は付けない):
" ポップアップを開いた時点で先頭候補が選択済みになり、
" Enter で即確定できる(IntelliJの挙動に最も近い)
set completeopt=menuone
set shortmess+=c
set ttimeout
set ttimeoutlen=50
" ---- スマートTab: 単語の途中なら補完、行頭・空白後なら通常のTab ----
function! s:SmartTab() abort
if pumvisible()
return "\<C-n>"
endif
let col = col('.') - 1
if col <= 0
return "\<Tab>"
endif
let ch = getline('.')[col - 1]
if ch =~ '[[:alnum:]_&%]'
return &omnifunc !=# '' ? "\<C-x>\<C-o>" : "\<C-n>"
endif
return "\<Tab>"
endfunction
inoremap <expr> <Tab> <SID>SmartTab()
inoremap <expr> <S-Tab> pumvisible() ? "\<C-p>" : "\<C-d>"
" ---- Ctrl+Space(端末では <C-@>)で補完起動 ----
inoremap <expr> <C-@> pumvisible() ? "\<C-n>" :
\ (&omnifunc !=# '' ? "\<C-x>\<C-o>" : "\<C-n>")
" GUI/gvim 用
inoremap <expr> <C-Space> pumvisible() ? "\<C-n>" :
\ (&omnifunc !=# '' ? "\<C-x>\<C-o>" : "\<C-n>")
" ============================================================
" コメントトグル(Ctrl+/ = 端末では <C-_>)
" ============================================================
" 'commentstring' (例: /* %s */) を使うので filetype を問わず動く
function! s:ToggleComment() range abort
let cms = &commentstring !=# '' ? &commentstring : '/* %s */'
let [lhs, rhs] = map(split(cms, '%s', 1), 'escape(trim(v:val), "*/\\")')
let lhs_txt = trim(split(&commentstring !=# '' ? &commentstring : '/* %s */', '%s', 1)[0])
let rhs_txt = trim(get(split(&commentstring !=# '' ? &commentstring : '/* %s */', '%s', 1), 1, ''))
" 選択範囲の非空行がすべてコメント済みかを判定
let all_commented = 1
for lnum in range(a:firstline, a:lastline)
let line = getline(lnum)
if line =~ '^\s*$'
continue
endif
if line !~ '^\s*' . lhs
let all_commented = 0
break
endif
endfor
for lnum in range(a:firstline, a:lastline)
let line = getline(lnum)
if line =~ '^\s*$'
continue
endif
if all_commented
" アンコメント
let line = substitute(line, '^\(\s*\)' . lhs . '\s\?', '\1', '')
if rhs !=# ''
let line = substitute(line, '\s\?' . rhs . '\s*$', '', '')
endif
else
" コメント化
let indent_part = matchstr(line, '^\s*')
let code_part = strpart(line, strlen(indent_part))
let line = indent_part . lhs_txt . ' ' . code_part
if rhs_txt !=# ''
let line .= ' ' . rhs_txt
endif
endif
call setline(lnum, line)
endfor
endfunction
nnoremap <silent> <C-_> :call <SID>ToggleComment()<CR>
vnoremap <silent> <C-_> :call <SID>ToggleComment()<CR>gv<Esc>
" ============================================================
" 行の移動(Alt+j / Alt+k)
" ============================================================
" 端末ではAltキーは ESC プレフィックスとして届くため、
" <M-j> が認識されるようにキーコードを登録する
if !has('gui_running')
execute "set <M-j>=\ej"
execute "set <M-k>=\ek"
endif
nnoremap <silent> <M-j> :m .+1<CR>==
nnoremap <silent> <M-k> :m .-2<CR>==
inoremap <silent> <M-j> <Esc>:m .+1<CR>==gi
inoremap <silent> <M-k> <Esc>:m .-2<CR>==gi
vnoremap <silent> <M-j> :m '>+1<CR>gv=gv
vnoremap <silent> <M-k> :m '<-2<CR>gv=gv
" ============================================================
" 行の複製(IntelliJのCtrl+D相当。Vimの<C-d>は温存し <leader>d に割当)
" ============================================================
nnoremap <silent> <leader>d :t.<CR>
vnoremap <silent> <leader>d :t'><CR>gv
" ============================================================
" ビジュアルモードのTabインデント(選択を維持)
" ============================================================
vnoremap <Tab> >gv
vnoremap <S-Tab> <gv
" ============================================================
" ステータスライン(モード・ファイル・種別・位置)
" ============================================================
set laststatus=2
set statusline=%f\ %m%r\ %y\ %{&fileencoding}\ %=%l:%c\ \ %p%%
SASIDE_INSTALLER_EOF
write_file 'plugin/sas_tools.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/plugin/sas_tools.vim
" グローバルコマンド。起動時に一度だけ読み込まれる(filetype判定とは無関係)。
if exists("g:loaded_sas_tools")
finish
endif
let g:loaded_sas_tools = 1
" 会社のSAS実行パスに合わせて ~/.vimrc 側で g:sas_command を上書きすること
if !exists("g:sas_command")
let g:sas_command = "sas"
endif
command! -nargs=0 SasRun call sas#run(expand('%:p'))
command! -nargs=0 SasLog call sas#openlog(expand('%:p:r') . '.log')
command! -nargs=0 SasLst call sas#openlst(expand('%:p:r') . '.lst')
command! -nargs=0 SasQuickfixLog call sas#quickfixlog(expand('%:p:r') . '.log')
SASIDE_INSTALLER_EOF
write_file 'plugin/saside.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: plugin/saside.vim
" Module: SASIDE 起動エントリ
" Description: オプション既定値・コマンド・キーマップ・autocmd の登録のみを
" 行う。ロジックは一切持たず、すべて autoload/saside/ に委譲する。
" このファイル以外は初回使用時までロードされない。
" =============================================================================
if exists('g:loaded_saside')
finish
endif
let g:loaded_saside = 1
let s:cpo_save = &cpo
set cpo&vim
" -----------------------------------------------------------------------------
" オプション既定値(vimrc での設定を尊重)
" -----------------------------------------------------------------------------
let g:saside_sas_cmd = get(g:, 'saside_sas_cmd', 'sas')
let g:saside_sas_args = get(g:, 'saside_sas_args', [])
let g:saside_txt_detect = get(g:, 'saside_txt_detect', 1)
let g:saside_txt_threshold = get(g:, 'saside_txt_threshold', 3)
let g:saside_index_maxlines = get(g:, 'saside_index_maxlines', 20000)
let g:saside_debounce_ms = get(g:, 'saside_debounce_ms', 300)
let g:saside_explorer_width = get(g:, 'saside_explorer_width', 28)
let g:saside_navigator_width = get(g:, 'saside_navigator_width', 34)
let g:saside_log_height = get(g:, 'saside_log_height', 10)
let g:saside_nav_stay = get(g:, 'saside_nav_stay', 0)
let g:saside_format_upcase = get(g:, 'saside_format_upcase', 0)
let g:saside_session_auto = get(g:, 'saside_session_auto', 0)
let g:saside_no_default_maps = get(g:, 'saside_no_default_maps', 0)
" -----------------------------------------------------------------------------
" コマンド(すべて autoload への薄い委譲)
" -----------------------------------------------------------------------------
" 実行・ログ
command! -nargs=? -complete=file SasRun call saside#run#Execute(<q-args>)
command! -nargs=? -complete=file SasRunTerm call saside#run#ExecuteTerm(<q-args>)
command! -range SasRunSelection call saside#run#ExecuteRange(<line1>, <line2>)
command! -nargs=0 SasRunBlock call saside#run#ExecuteBlock()
command! -nargs=0 SasCheck call saside#run#Check()
command! -nargs=0 SasAbort call saside#run#Abort()
command! -nargs=? -complete=file SasLog call saside#log#Open(<q-args>)
command! -nargs=? -complete=file SasLst call saside#log#Lst(<q-args>)
command! -nargs=0 SasLogErrors call saside#log#Quickfix('error')
command! -nargs=0 SasLogWarnings call saside#log#Quickfix('warning')
command! -nargs=0 SasLogNotes call saside#log#Quickfix('note')
command! -nargs=0 SasQuickfix call saside#log#Quickfix('all')
command! -nargs=? -complete=file SasLogRisk call saside#log#Risk(<q-args>)
" ビュー
command! -nargs=0 SasExplorer call saside#explorer#Toggle()
command! -nargs=0 SasNav call saside#navigator#Toggle('nav')
command! -nargs=0 SasOutline call saside#outline#Toggle()
command! -nargs=0 SasDatasets call saside#dataset#Toggle()
command! -nargs=0 SasMacros call saside#macro#Toggle()
command! -nargs=0 SasIde call saside#layout#Ide()
" 解析
command! -nargs=0 SasIndex echo saside#parser#Index()
command! -nargs=0 SasBlockInfo echo saside#parser#BlockAt(line('.'))
" 整形・テンプレート
command! -nargs=0 SasFormat call saside#format#Format()
command! -nargs=1 -complete=custom,saside#template#Kinds
\ SasNew call saside#template#New(<q-args>)
" 検索
command! -nargs=1 SasGrep call saside#search#Grep(<q-args>)
command! -nargs=? SasFindDs call saside#search#Dataset(<q-args>)
command! -nargs=? SasFindMacro call saside#search#Macro(<q-args>)
command! -nargs=0 SasTodo call saside#todo#List()
" ブックマーク・セッション
command! -nargs=? SasBookmark call saside#bookmark#Add(<q-args>)
command! -nargs=0 SasBookmarks call saside#bookmark#List()
command! -nargs=0 SasBookmarkClear call saside#bookmark#Clear()
command! -nargs=? SasSessionSave call saside#session#Save(<q-args>)
command! -nargs=? SasSessionLoad call saside#session#Load(<q-args>)
" ヘルプ
command! -nargs=? SasHelp call saside#help#Lookup(<q-args>)
" -----------------------------------------------------------------------------
" <Plug> マッピングと既定キー
" -----------------------------------------------------------------------------
nnoremap <silent> <Plug>(saside-explorer) :SasExplorer<CR>
nnoremap <silent> <Plug>(saside-nav) :SasNav<CR>
nnoremap <silent> <Plug>(saside-outline) :SasOutline<CR>
nnoremap <silent> <Plug>(saside-run) :SasRun<CR>
xnoremap <silent> <Plug>(saside-run-selection) :SasRunSelection<CR>
nnoremap <silent> <Plug>(saside-run-block) :SasRunBlock<CR>
nnoremap <silent> <Plug>(saside-log) :call saside#log#Toggle()<CR>
nnoremap <silent> <Plug>(saside-lst) :SasLst<CR>
nnoremap <silent> <Plug>(saside-quickfix) :SasQuickfix<CR>
nnoremap <silent> <Plug>(saside-check) :SasCheck<CR>
nnoremap <silent> <Plug>(saside-log-risk) :SasLogRisk<CR>
nnoremap <silent> <Plug>(saside-format) :SasFormat<CR>
nnoremap <silent> <Plug>(saside-todo) :SasTodo<CR>
nnoremap <silent> <Plug>(saside-ide) :SasIde<CR>
nnoremap <silent> <Plug>(saside-grep) :call saside#search#Grep(input('SasGrep: '))<CR>
nnoremap <silent> <Plug>(saside-find-ds) :SasFindDs<CR>
nnoremap <silent> <Plug>(saside-find-mac) :SasFindMacro<CR>
nnoremap <silent> <Plug>(saside-bm-add) :SasBookmark<CR>
nnoremap <silent> <Plug>(saside-bm-list) :SasBookmarks<CR>
" <C-r>= 方式(snipMateと同じ): <C-o>だと行末より後のカーソル位置が
" 保持されず、行末で発動した際にキーの最後の1文字を取りこぼすため
inoremap <silent> <Plug>(saside-snippet) <C-r>=saside#snippet#ExpandOrJump()<CR>
if !g:saside_no_default_maps
nmap <F2> <Plug>(saside-explorer)
nmap <F3> <Plug>(saside-nav)
nmap <F4> <Plug>(saside-outline)
nmap <F5> <Plug>(saside-run)
" ビジュアル選択中の F5 は「選択範囲だけ実行」
xmap <F5> <Plug>(saside-run-selection)
" カーソル位置の DATA/PROC ステップだけ実行
nmap <leader>rb <Plug>(saside-run-block)
nmap <F6> <Plug>(saside-log)
nmap <F7> <Plug>(saside-lst)
nmap <F8> <Plug>(saside-quickfix)
" Shift+F5 相当が使えない端末が多いので Leader 系に割当
nmap <leader>rc <Plug>(saside-check)
nmap <leader>lr <Plug>(saside-log-risk)
nmap <F9> <Plug>(saside-format)
nmap <F10> <Plug>(saside-todo)
nmap <F12> <Plug>(saside-ide)
nmap <leader>sg <Plug>(saside-grep)
nmap <leader>sd <Plug>(saside-find-ds)
nmap <leader>sm <Plug>(saside-find-mac)
nmap <leader>bb <Plug>(saside-bm-add)
nmap <leader>bl <Plug>(saside-bm-list)
imap <C-j> <Plug>(saside-snippet)
endif
" -----------------------------------------------------------------------------
" autocmd: イベント配線・インデックスのデバウンス更新
" -----------------------------------------------------------------------------
function! s:schedule_index() abort
if line('$') > g:saside_index_maxlines
return
endif
if has('timers')
if exists('s:index_timer')
call timer_stop(s:index_timer)
endif
let bufnr = bufnr('%')
let s:index_timer = timer_start(g:saside_debounce_ms,
\ {-> bufexists(bufnr) ? saside#parser#Index(bufnr) : 0})
else
call saside#parser#Index()
endif
endfunction
" ナビゲータが「ロード済みの場合のみ」再描画する(未ロードなら何もしない=
" ペインを開いたことが無いのだから更新も不要)
function! s:nav_refresh() abort
if exists('*saside#navigator#OnIndexUpdated')
call saside#navigator#OnIndexUpdated(g:saside_event)
endif
endfunction
augroup saside_core
autocmd!
" インデックス更新のデバウンス
autocmd FileType sas autocmd! saside_buf TextChanged,InsertLeave <buffer>
\ call s:schedule_index()
" イベント配線
autocmd User saside:RunFinished call saside#log#OnRunFinished(g:saside_event)
autocmd User saside:RunStarted call saside#statusline#OnRun(g:saside_event)
autocmd User saside:RunFinished call saside#statusline#OnRun(g:saside_event)
autocmd User saside:IndexUpdated call s:nav_refresh()
" ステータスライン・omnifunc
autocmd FileType sas call saside#statusline#Enable()
\ | setlocal omnifunc=saside#complete#Omni
" 終了時の後始末
autocmd VimLeavePre * if g:saside_session_auto
\ | silent! call saside#session#Save('auto')
\ | endif
augroup END
augroup saside_buf
autocmd!
augroup END
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_INSTALLER_EOF
write_file 'syntax/sas.vim' <<'SASIDE_INSTALLER_EOF'
" Vim syntax file
" Language: SAS 9.x (data step / macro / PROC SQL)
" Maintainer: ~/.vim only, no system runtime dependency
" Filenames: *.sas
"
" 設計方針:
" - SASはキーワードの大文字小文字を区別しないため syntax case ignore を使用
" - コメント/文字列/日付定数はすべて region ベースにして複数行にまたがっても
" 正しく認識できるようにしている(match のみだと1行しか拾えない)
" - マクロ関連は "%" が 'iskeyword' に含まれないため syntax keyword ではなく
" syntax match で実装し、より具体的なグループを後から定義することで
" 優先度を上書きしている(Vimは同じ位置で重なるmatch/regionは
" 「後で定義した方が勝つ」ため)
if exists("b:current_syntax")
finish
endif
syntax case ignore
syntax sync minlines=100
" ============================================================
" コメント
" ============================================================
syntax region sasComment start="/\*" end="\*/" contains=@Spell fold
syntax region sasComment start="^\s*\*" end=";" contains=@Spell
syntax region sasComment start=";\s*\*"lc=1 end=";" contains=@Spell
" ============================================================
" 文字列
" ============================================================
syntax region sasString start=+"+ skip=+""+ end=+"+ contains=sasMacroVar
syntax region sasString start=+'+ skip=+''+ end=+'+
" 日付・時刻・日時リテラル ('01JAN2024'd "01JAN2024:00:00:00"dt '08:30:00't)
syntax match sasDateConst "'[^']\{-}'\c[dt]\{1,2}\>"
syntax match sasDateConst "\"[^\"]\{-}\"\c[dt]\{1,2}\>"
" 16進定数 'FF'x / 2進定数 '1010'b
syntax match sasHexConst "'[0-9A-Fa-f]\+'\cx\>"
syntax match sasBitConst "'[01]\+'\cb\>"
" ============================================================
" 数値(整数・小数・指数表記)
" ============================================================
syntax match sasNumber "\<[0-9]\+\(\.[0-9]*\)\?\([eE][+-]\?[0-9]\+\)\?\>"
syntax match sasNumber "\<\.[0-9]\+\([eE][+-]\?[0-9]\+\)\?\>"
" フォーマット/インフォーマット表記 (comma12.2 / yymmdd10. / $char20. / date9.)
syntax match sasFormat "\<\$\?[A-Za-z_][A-Za-z0-9_]*[0-9]\+\.\d*\>"
syntax match sasFormat "\<\$\?[A-Za-z_][A-Za-z0-9_]*\.\>"
" ============================================================
" マクロ変数 &var &var. &&var &&var&i.(間接参照)
" ============================================================
syntax match sasMacroVar "&\{1,2}[A-Za-z_][A-Za-z0-9_]*\.\?"
" ============================================================
" マクロ呼び出し(総称)→ 後段でより具体的なグループが上書きする
" ============================================================
syntax match sasMacroCall "%[A-Za-z_][A-Za-z0-9_]*"
" マクロ制御構文
syntax match sasMacroCond "%\c\(if\|then\|else\|do\|end\|until\|while\|to\|goto\)\>"
" マクロ宣言・定義
syntax match sasMacroDecl "%\c\(macro\|mend\|let\|global\|local\|return\|abort\|put\|putlog\|window\|display\|input\|include\|inc\|syscall\)\>"
" マクロ関数
syntax match sasMacroFunc "%\c\(scan\|qscan\|substr\|qsubstr\|index\|length\|upcase\|qupcase\|lowcase\|left\|trim\|qtrim\|cmpres\|sysfunc\|qsysfunc\|sysevalf\|eval\|str\|nrstr\|bquote\|nrbquote\|quote\|unquote\|superq\|sysget\|symexist\|symglobl\|symlocal\|sysprod\|sysfunc\)\>"
" ============================================================
" データステップ / 一般ステートメント
" ============================================================
syntax keyword sasStatement
\ data proc run quit options libname filename footnote footnote1
\ footnote2 title title1 title2 goto link ods proc
syntax keyword sasStatement
\ set merge update modify by output input cards cards4 datalines
\ datalines4 infile file put putlog list
syntax keyword sasStatement
\ array retain drop keep rename length format informat label attrib
\ where
" 制御構文(Conditional / Repeat として区別)
syntax keyword sasConditional if then else select when otherwise
syntax keyword sasRepeat do end until while
" 演算子(ニーモニック)
syntax keyword sasOperator eq ne lt gt le ge in not and or
" 自動変数
syntax keyword sasAutoVar _n_ _error_ _all_ _character_ _numeric_ _null_
" CALL ルーチン(call symput / call execute 等の呼び出し)
syntax keyword sasCallStmt call
" ============================================================
" PROC 名
" ============================================================
syntax keyword sasProcName
\ sql means summary freq print report sort transpose datasets
\ contents append compare corr univariate rank standardize
\ sgplot sgpanel gplot gchart tabulate export import copy
\ reg glm logistic genmod mixed phreg lifetest factor cluster
\ fastclus timeseries arima hpforecast optmodel iml fcmp
\ hplogistic hpsplit hpforest stdize score template http json
\ groovy lua ds2 catalog sgrender boxplot
" ============================================================
" PROC SQL 内キーワード
" ============================================================
" when/then/else/end はデータステップ側(sasConditional/sasRepeat)の
" キーワードと重複するため、ここには含めない(重複させると定義順で
" 上書きされ、同じ単語が文脈によって色が変わってしまい一貫性が失われるため)
syntax keyword sasSqlKw
\ select from where group having order create table view as
\ insert into update delete drop alter distinct union intersect
\ except case between like is null exists
\ inner left right full outer cross join on count sum avg min max
\ asc desc all any some using calculated reset feedback noprint
" ============================================================
" 関数(データステップ / マクロ双方でよく使うもの)
" ============================================================
syntax keyword sasFunction
\ substr scan compress upcase lowcase propcase trim ltrim strip
\ left right reverse repeat translate tranwrd index indexc find
\ findc anyalpha anydigit anypunct anyspace verify input put
\ vvalue vname sum mean std var n nmiss round ceil floor int mod
\ abs sqrt exp log log10 log2 intck intnx datepart timepart today
\ date time datdif yrdif year month day qtr week weekday hour
\ minute second mdy hms dhms symget symput symputx open close
\ attrn attrc varnum varname varlabel varfmt varlen fetch fetchobs
\ exist libref pathname dequote quote resolve
" ============================================================
" ハイライトリンク
" ============================================================
highlight default link sasComment Comment
highlight default link sasString String
highlight default link sasDateConst Constant
highlight default link sasHexConst Constant
highlight default link sasBitConst Constant
highlight default link sasNumber Number
highlight default link sasFormat Special
highlight default link sasMacroVar Identifier
highlight default link sasMacroCall PreProc
" PreCondit は多くの環境で色未設定のことがあるため、確実に見える
" PreProc へフォールバックさせておく(syncolor.vim側でPreCondit専用の色を
" 定義していればそちらが優先される。default linkは既に色がある場合は上書きしない)
highlight default link PreCondit PreProc
highlight default link sasMacroCond PreCondit
highlight default link sasMacroDecl PreProc
highlight default link sasMacroFunc Function
highlight default link sasStatement Statement
highlight default link sasConditional Conditional
highlight default link sasRepeat Repeat
highlight default link sasOperator Operator
highlight default link sasAutoVar Special
highlight default link sasCallStmt Statement
highlight default link sasProcName Type
highlight default link sasSqlKw Statement
highlight default link sasFunction Function
let b:current_syntax = "sas"
SASIDE_INSTALLER_EOF
write_file 'syntax/saslog.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/syntax/saslog.vim
" SASログ(.log)のERROR/WARNING/NOTEを色分けする
if exists("b:current_syntax")
finish
endif
syntax match sasLogNote '^\d*\s*NOTE:.*'
syntax match sasLogWarning '^\d*\s*WARNING:.*'
syntax match sasLogError '^\d*\s*ERROR\(\s\d\+-\d\+\)\?:.*'
syntax match sasLogInfo '^\d*\s*INFO:.*'
syntax match sasLogNumber '^\d\+' nextgroup=sasLogNote,sasLogWarning,sasLogError
highlight default link sasLogNote Comment
highlight default link sasLogWarning WarningMsg
highlight default link sasLogError ErrorMsg
highlight default link sasLogInfo Identifier
highlight default link sasLogNumber LineNr
let b:current_syntax = "saslog"
SASIDE_INSTALLER_EOF
write_file 'syntax/saslst.vim' <<'SASIDE_INSTALLER_EOF'
" =============================================================================
" File: syntax/saslst.vim
" Module: SASリスト出力(.lst)の色分け
" =============================================================================
if exists('b:current_syntax')
finish
endif
" タイトル行(The SAS System / TITLEステートメントの出力)
syntax match saslstTitle /^\s*The SAS System.*/
syntax match saslstTitle /^\s{10,}\S.*\%<4l/
" 改ページ (^L)
syntax match saslstPage /\%x0c/
" 罫線・区切り
syntax match saslstRule /^[-=_+|]\{4,}$/
" 観測番号列
syntax match saslstObsnum /^\s*\d\+\s/
highlight default link saslstTitle Title
highlight default link saslstPage NonText
highlight default link saslstRule Comment
highlight default link saslstObsnum LineNr
let b:current_syntax = 'saslst'
SASIDE_INSTALLER_EOF
write_file 'syntax/syncolor.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/syntax/syncolor.vim
" 既定のハイライトグループ色をリンクする最小版。
" `default link` を使っているため、システム版 syncolor.vim や
" colorscheme が先に色を設定していればそちらを尊重し、上書きしない。
if &background ==# 'dark'
highlight default Comment ctermfg=244 guifg=#888888
highlight default Constant ctermfg=150 guifg=#afd787
highlight default String ctermfg=150 guifg=#afd787
highlight default Identifier ctermfg=111 guifg=#87afff
highlight default Statement ctermfg=214 guifg=#ffaf00 cterm=bold gui=bold
highlight default PreProc ctermfg=170 guifg=#d787d7
highlight default Type ctermfg=81 guifg=#5fd7ff
highlight default Special ctermfg=203 guifg=#ff5f5f
else
highlight default Comment ctermfg=DarkGray guifg=#666666
highlight default Constant ctermfg=DarkGreen guifg=#006400
highlight default String ctermfg=DarkGreen guifg=#006400
highlight default Identifier ctermfg=DarkBlue guifg=#00008b
highlight default Statement ctermfg=DarkYellow guifg=#8b6914 cterm=bold gui=bold
highlight default PreProc ctermfg=DarkMagenta guifg=#8b008b
highlight default Type ctermfg=DarkCyan guifg=#008b8b
highlight default Special ctermfg=DarkRed guifg=#8b0000
endif
highlight default link Boolean Constant
highlight default link Number Constant
highlight default link Function Identifier
highlight default link Keyword Statement
highlight default link Repeat Statement
highlight default link Conditional Statement
highlight default link Label Statement
highlight default link Operator Statement
SASIDE_INSTALLER_EOF
write_file 'syntax/syntax.vim' <<'SASIDE_INSTALLER_EOF'
" ~/.vim/syntax/syntax.vim
" これがエラーの本体だった E484 を解消するファイル。
" $VIMRUNTIME/syntax/syntax.vim が存在しない環境で :syntax on を
" 成立させるための最小ブートストラップ。
" 自分以外の runtimepath 上に syntax/syntax.vim があればそちらに委譲する。
if exists("did_load_syntax")
finish
endif
let s:self = fnamemodify(expand('<sfile>:p'), ':p')
let s:other_exists = 0
for s:f in globpath(&runtimepath, 'syntax/syntax.vim', 0, 1)
if fnamemodify(s:f, ':p') !=# s:self
let s:other_exists = 1
break
endif
endfor
unlet! s:self s:f
if s:other_exists
unlet s:other_exists
finish
endif
unlet s:other_exists
let did_load_syntax = 1
let s:cpo_save = &cpo
set cpo&vim
" FileType が確定したら syntax/<ft>.vim を読みに行く配線
augroup syntaxset
au!
au FileType * call s:SynSet()
augroup END
function! s:SynSet() abort
if exists('b:current_syntax')
unlet b:current_syntax
endif
let s = expand("<amatch>")
if s !=# ""
let name = substitute(s, '\.', '_', 'g')
exe 'runtime! syntax/' . name . '.vim syntax/' . name . '/*.vim'
endif
endfunction
" 既定ハイライトグループ(本来 syncolor.vim が担う部分)
runtime! syntax/syncolor.vim
let &cpo = s:cpo_save
unlet s:cpo_save
let g:syntax_on = 1
SASIDE_INSTALLER_EOF
write_file 'vimrc.example' <<'SASIDE_INSTALLER_EOF'
" ============================================================
" SAS Vim環境 — ~/.vimrc 追記内容
" 既に同じ設定がある場合は重複行に注意して反映してください
" ============================================================
set nocompatible
" ~/.vim を runtimepath の先頭付近に、~/.vim/after を末尾に確実に置く
set runtimepath^=~/.vim
set runtimepath+=~/.vim/after
filetype plugin indent on
syntax on
" --- 表示・操作の基本 ---
set number
set ruler
set showcmd
set laststatus=2
set hidden
set incsearch
set hlsearch
set ignorecase
set smartcase
set wildmenu
set backspace=indent,eol,start
set encoding=utf-8
set fileencodings=utf-8,cp932,euc-jp,sjis
set tabstop=4
set shiftwidth=4
set expandtab
set autoindent
" 折り畳みは定義するが、ファイルを開いた時点ではすべて展開しておく
set foldlevelstart=99
silent! colorscheme sasdark
set path+=**
set wildignore+=*.sas7bdat,*.sas7bcat
" ============================================================
" 運用向け設定(推奨)
" ============================================================
" --- ペースト対策: F11 で貼り付けモードを切替 ---
" 他所からコピペする際に F11 を押してから貼ると、括弧補完等が暴発せず
" 一瞬で貼り付けられる(貼り終えたら再度 F11 で戻す)
set pastetoggle=<F11>
" --- 他人のファイルの modeline で設定を書き換えられないようにする ---
set nomodeline
" --- 外部で更新されたファイルを自動再読込(SASログの追随に有効)---
set autoread
augroup sasvim_autoread
autocmd!
autocmd CursorHold,CursorHoldI,FocusGained,BufEnter * silent! checktime
augroup END
" --- 永続undo・swap・backup を ~/.vim/.state 配下に集約 ---
" 作業ディレクトリに .swp が散らばるのを防ぎ、Vimを閉じてもundoが残る
let s:state = expand('~/.vim/.state')
for s:sub in ['undo', 'swap', 'backup']
if !isdirectory(s:state . '/' . s:sub)
call mkdir(s:state . '/' . s:sub, 'p', 0700)
endif
endfor
if has('persistent_undo')
set undofile
let &undodir = s:state . '/undo'
endif
let &directory = s:state . '/swap//'
let &backupdir = s:state . '/backup//'
set backup
" --- 巨大ログ対策: 一定サイズ超のファイルは重い機能を自動オフ ---
" SASバッチのログは数百MBになり得るため、開くだけで固まるのを防ぐ
let g:sasvim_bigfile_mb = 10
augroup sasvim_bigfile
autocmd!
autocmd BufReadPre * if getfsize(expand('<afile>')) > g:sasvim_bigfile_mb * 1024 * 1024
\ | setlocal noswapfile noundofile bufhidden=unload
\ | setlocal syntax=off
\ | endif
augroup END
" ============================================================
" SAS実行コマンド(PATHが通っていれば設定不要)
" ============================================================
" 会社の sas 実体がPATHに無い / 別名の場合はフルパスを指定する
" let g:saside_sas_cmd = '/opt/sasfoundation/SASFoundation/9.3/sas'
" 追加引数が必要なら(例: 作業領域の指定)
" let g:saside_sas_args = ['-work', '/your/work/dir']
SASIDE_INSTALLER_EOF
# ===== 整合性マニフェスト(自動生成。編集しないこと)=====
ng=0
check() {
p="$TARGET/$1"; expL=$2; expB=$3
actL=$(wc -l < "$p" | tr -d " "); actB=$(wc -c < "$p" | tr -d " ")
if [ "$actL" != "$expL" ] || [ "$actB" != "$expB" ]; then
echo " NG: $1 (expected ${expL}L/${expB}B -> got ${actL}L/${actB}B)"
ng=$((ng+1))
fi
}
check 'autoload/sas.vim' 97 3450
check 'autoload/saside/bookmark.vim' 75 2234
check 'autoload/saside/complete.vim' 157 4731
check 'autoload/saside/dataset.vim' 7 380
check 'autoload/saside/event.vim' 72 2705
check 'autoload/saside/explorer.vim' 249 8182
check 'autoload/saside/format.vim' 64 2201
check 'autoload/saside/help.vim' 28 940
check 'autoload/saside/layout.vim' 94 3162
check 'autoload/saside/log.vim' 208 7883
check 'autoload/saside/macro.vim' 7 370
check 'autoload/saside/navigator.vim' 171 6003
check 'autoload/saside/outline.vim' 7 382
check 'autoload/saside/parser.vim' 347 13247
check 'autoload/saside/run.vim' 191 7483
check 'autoload/saside/search.vim' 59 2068
check 'autoload/saside/session.vim' 81 2629
check 'autoload/saside/snippet.vim' 111 3509
check 'autoload/saside/statusline.vim' 70 2528
check 'autoload/saside/template.vim' 76 2394
check 'autoload/saside/todo.vim' 13 425
check 'autoload/saside/util.vim' 104 3793
check 'autoload/sasomni.vim' 201 6783
check 'colors/sasdark.vim' 77 3265
check 'dict/sas_formats.dict' 23 564
check 'dict/sas_functions.dict' 107 3263
check 'dict/sas_macros.dict' 46 1515
check 'dict/sas_ods.dict' 20 732
check 'dict/sas_options.dict' 34 1331
check 'dict/sas_procs.dict' 44 1464
check 'dict/snippets.txt' 44 1000
check 'doc/saside.txt' 142 7877
check 'filetype.vim' 36 1098
check 'ftdetect/sas.vim' 48 1759
check 'ftplugin.vim' 43 1105
check 'ftplugin/sas.vim' 73 2751
check 'ftplugin/saslog.vim' 23 1023
check 'indent.vim' 42 919
check 'indent/sas.vim' 232 8291
check 'plugin/autopairs.vim' 116 3509
check 'plugin/ide.vim' 143 5338
check 'plugin/sas_tools.vim' 16 650
check 'plugin/saside.vim' 181 8740
check 'syntax/sas.vim' 168 8272
check 'syntax/saslog.vim' 19 674
check 'syntax/saslst.vim' 24 849
check 'syntax/syncolor.vim' 33 1649
check 'syntax/syntax.vim' 55 1341
check 'vimrc.example' 95 3198
if [ "$ng" -gt 0 ]; then
echo ""
echo "!!! $ng file(s) are CORRUPTED (lines lost in copy/transfer)."
echo "!!! 記事のインストーラを再コピーしてやり直してください。"
echo "!!! 退避済みの元ファイルは $BACKUP にあります。"
exit 1
else
echo "整合性チェック: 全 $count ファイル OK"
fi
echo ""
echo "完了: $count ファイルを展開しました"
[ -d "$BACKUP" ] && echo "既存ファイルの退避先: $BACKUP"
printf '\n次の手順:\n'
printf ' 1. ~/.vim/vimrc.example の内容を ~/.vimrc に反映(新規なら cp)\n'
printf ' 2. vim -c "helptags ~/.vim/doc" -c q\n'
printf ' 3. .sasファイルを開いて F12 と構文チェックを試す\n'
追加パッチ
#!/bin/sh
# saside_patch2.sh — SAS Vim環境 インデント修正パッチ(第2弾)
#
# 前提: saside_install.sh → saside_patch.sh の順で適用済みであること
# 使い方: sh saside_patch2.sh
# 注意: sudo不要($HOME/.vim にしか書き込みません。既存ファイルは自動退避)
#
# --------------------------------------------------------------------------
# 修正する不具合
# --------------------------------------------------------------------------
# [根本原因] 従来の indent/sas.vim は「コメントかどうか」の判定を
# syntax(構文ハイライト)の synID() に依存していた。
# syntax が効いていない環境(colorscheme未適用・巨大ファイルで
# syntax=off・:syntax on 失敗)ではコメント内が「コード」と
# 誤認識され、以降の行が芋づる式にインデントされる。
#
# 1. /*====== ... ======*/ 形式のバナーコメントの中身が
# 「;で終わらない継続行」と誤判定され、直後のコードが引きずられる
# 2. 文字列リテラル内の /* や ( がコード扱いされ、括弧ルールが暴走する
# 3. PROC SQL の select 文が DATAステップの SELECT ブロックと誤認され、
# 以降が延々と深くなる(select ... ; で終わる行が該当)
# 4. data= オプション(proc means data=x)が DATAステップ開始と誤認される
# 5. datalines/cards の生データ行が勝手に整形される
# 6. else が直前行に揃ってしまい、対応する if とずれる
# 7. 1行完結の if x then do; ... end; で +1段ずれる
# 8. %else / when / otherwise の再インデント契機が無い
# 9. :SasFormat の予約語大文字化が、syntax未適用時に
# 文字列やコメントの中身まで大文字化してしまう
# --------------------------------------------------------------------------
set -e
TARGET="$HOME/.vim"
BACKUP="$HOME/.vim_patch2backup_$(date +%Y%m%d_%H%M%S)"
count=0
if [ ! -d "$TARGET/autoload/saside" ]; then
echo "エラー: $TARGET/autoload/saside がありません。"
echo "先に saside_install.sh と saside_patch.sh を実行してください。"
exit 1
fi
write_file() {
path="$TARGET/$1"; dir=$(dirname "$path"); mkdir -p "$dir"
if [ -f "$path" ]; then mkdir -p "$BACKUP/$(dirname "$1")"; cp -p "$path" "$BACKUP/$1"; fi
cat > "$path"; count=$((count + 1))
}
echo "インデント修正パッチを適用します: $TARGET"
# =============================================================================
write_file 'autoload/sas/code.vim' <<'SASIDE_PATCH_EOF'
" =============================================================================
" File: autoload/sas/code.vim
" Module: code — SASコードの字句状態判定(syntax非依存)
"
" なぜ必要か:
" 従来 indent/sas.vim は synID() で「コメント/文字列の中か」を判定していた。
" しかし syntax は
" - colorscheme や :syntax on が失敗している
" - 巨大ファイル対策で syntax=off にしている
" - syntax sync の同期ずれ
" といった理由で当てにならないことがある。その場合コメントの中身が
" コードとして扱われ、インデントが芋づる式に壊れる。
"
" 本モジュールはバッファのテキストだけを見て
" - ブロックコメント /* */ の内側か
" - 星コメント * ... ; の内側か
" - 文字列リテラルの内側か
" を判定する。indent と format の双方がこれを使う。
"
" 公開API:
" sas#code#StateAtLineStart(lnum) 行頭時点でブロックコメント内か (0/1)
" sas#code#Text(lnum) コメント・文字列を除去した「コード」
" sas#code#InComment(lnum) 行全体がコメントの内側か
" sas#code#InCommentOrString(l, c) 指定位置がコメント/文字列の中か
" sas#code#InDataLines(lnum) datalines/cards の生データ部か
" sas#code#InProcSql(lnum) PROC SQL ~ QUIT; の内側か
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
" 後方探索の上限行数(性能と正確さのトレードオフ)
function! s:lookback() abort
return get(g:, 'sas_code_lookback', 400)
endfunction
" -----------------------------------------------------------------------------
" 1行分の字句走査
" -----------------------------------------------------------------------------
" a:line を a:st(0=地の文 / 1=ブロックコメント内)から走査し、
" {'code': コード部分, 'st': 行末の状態, 'star': 星コメント継続中か} を返す。
"
" code の作り方:
" - コメントは空白1個に潰す
" - 文字列リテラルは '' / "" に潰す(中身の ( ; /* を無害化しつつ
" 「そこに何かある」という構造は残す)
function! s:scan(line, st) abort
let out = ''
let n = strlen(a:line)
let i = 0
let st = a:st
let q = ''
let star = 0
while i < n
let ch = a:line[i]
let nx = i + 1 < n ? a:line[i + 1] : ''
" --- ブロックコメントの中 ---
if st == 1
if ch ==# '*' && nx ==# '/'
let st = 0
let out .= ' '
let i += 2
continue
endif
let i += 1
continue
endif
" --- 星コメント(* ... ;)の中 ---
if star
if ch ==# ';'
let star = 0
let out .= ';'
endif
let i += 1
continue
endif
" --- 文字列の中 ---
if q !=# ''
if ch ==# q
let q = ''
endif
let i += 1
continue
endif
" --- ブロックコメント開始 ---
if ch ==# '/' && nx ==# '*'
let st = 1
let out .= ' '
let i += 2
continue
endif
" --- 文字列開始('' / "" に潰して残す)---
if ch ==# "'" || ch ==# '"'
let q = ch
let out .= ch . ch
let i += 1
continue
endif
" --- 星コメント開始(文の先頭位置の * のみ)---
" 掛け算やべき乗の * と区別するため、
" 「行頭から空白のみ」または「直前が ;」の場合だけコメントとみなす
if ch ==# '*' && (out =~# '^\s*$' || out =~# ';\s*$')
let star = 1
let i += 1
continue
endif
let out .= ch
let i += 1
endwhile
return {'code': out, 'st': st, 'star': star}
endfunction
" -----------------------------------------------------------------------------
" 指定位置のマーカーが文字列の中かどうか(1行内の簡易判定)
" -----------------------------------------------------------------------------
function! s:marker_in_string(lnum, col) abort
let line = getline(a:lnum)
if line !~# "['\"]"
return 0
endif
let q = ''
let i = 0
while i < a:col - 1
let ch = line[i]
if q ==# ''
if ch ==# "'" || ch ==# '"'
let q = ch
endif
elseif ch ==# q
let q = ''
endif
let i += 1
endwhile
return q !=# ''
endfunction
" -----------------------------------------------------------------------------
" 行頭時点でブロックコメントの内側か
" -----------------------------------------------------------------------------
" 実装: 直前の /* と */ のうち「後に現れた方」で判定する。
" search() はC実装なので数百行の後方探索でも十分速い。
function! sas#code#StateAtLineStart(lnum) abort
if a:lnum <= 1
return 0
endif
let stop = max([1, a:lnum - s:lookback()])
let save = getcurpos()
call cursor(a:lnum, 1)
let st = 0
let guard = 0
while guard < 50
let [l, c] = searchpos('/\*\|\*/', 'bW', stop)
if l == 0
break
endif
if !s:marker_in_string(l, c)
let st = strpart(getline(l), c - 1, 2) ==# '/*' ? 1 : 0
break
endif
let guard += 1
endwhile
call setpos('.', save)
return st
endfunction
" -----------------------------------------------------------------------------
" 星コメント(* ... ;)が前の行から継続しているか
" -----------------------------------------------------------------------------
function! s:in_star_comment(lnum) abort
let l = a:lnum - 1
let limit = max([1, a:lnum - 20])
while l >= limit
let raw = getline(l)
if raw =~# '^\s*$'
let l -= 1
continue
endif
" ブロックコメント内の行なら星コメントとは無関係
if sas#code#StateAtLineStart(l) == 1
return 0
endif
" ; が出ていれば星コメントはそこで閉じている
if raw =~# ';'
return 0
endif
" ; を挟まずに * で始まる行に到達したら継続中
if raw =~# '^\s*\*'
return 1
endif
let l -= 1
endwhile
return 0
endfunction
" -----------------------------------------------------------------------------
" コード部分の取得(行キャッシュ付き)
" -----------------------------------------------------------------------------
function! sas#code#Text(lnum) abort
let tick = b:changedtick
if !exists('b:sas_code_cache') || get(b:, 'sas_code_tick', -1) != tick
let b:sas_code_cache = {}
let b:sas_code_tick = tick
endif
let key = string(a:lnum)
if has_key(b:sas_code_cache, key)
return b:sas_code_cache[key]
endif
let raw = getline(a:lnum)
if raw =~# '^\s*$'
let b:sas_code_cache[key] = ''
return ''
endif
if s:in_star_comment(a:lnum)
let b:sas_code_cache[key] = ''
return ''
endif
let r = s:scan(raw, sas#code#StateAtLineStart(a:lnum))
let b:sas_code_cache[key] = r.code
return r.code
endfunction
" 行全体がコメントの内側か(バナーコメントの中身・閉じ行を含む)
function! sas#code#InComment(lnum) abort
if sas#code#StateAtLineStart(a:lnum) == 1
return 1
endif
return s:in_star_comment(a:lnum)
endfunction
" 指定位置がコメントまたは文字列の中か(searchpair の skip 用)
function! sas#code#InCommentOrString(lnum, col) abort
let st = sas#code#StateAtLineStart(a:lnum)
if st == 1
" 行頭がコメント内。*/ より手前なら確実にコメント内
let close = match(getline(a:lnum), '\*/')
if close == -1 || a:col - 1 <= close + 1
return 1
endif
endif
if s:in_star_comment(a:lnum)
return 1
endif
" 行頭から指定桁までを走査して、その時点の状態を見る
let head = strpart(getline(a:lnum), 0, a:col - 1)
let r = s:scan(head, st)
if r.st == 1 || r.star
return 1
endif
" 文字列の中かどうかは scan では戻さないので簡易判定
return s:marker_in_string(a:lnum, a:col)
endfunction
" -----------------------------------------------------------------------------
" datalines / cards の生データ部か
" -----------------------------------------------------------------------------
function! sas#code#InDataLines(lnum) abort
let save = getcurpos()
call cursor(a:lnum, 1)
let s = search('\c^\s*\%(cards4\?\|datalines4\?\|parmcards\)\s*;', 'bnW',
\ max([1, a:lnum - 500]))
call setpos('.', save)
if s == 0
return 0
endif
" 開始行と現在行の間に終端(; または ;;;;)があれば既に閉じている
for l in range(s + 1, a:lnum - 1)
if getline(l) =~# '^\s*;\{1,4}\s*$'
return 0
endif
endfor
return 1
endfunction
" -----------------------------------------------------------------------------
" PROC SQL の内側か(SQLのSELECTをDATAステップのSELECTと誤認しないため)
" -----------------------------------------------------------------------------
function! sas#code#InProcSql(lnum) abort
let save = getcurpos()
call cursor(a:lnum, 1)
let stop = max([1, a:lnum - 500])
let p = search('\c^\s*proc\s\+sql\>', 'bnW', stop)
let q = search('\c^\s*\%(quit\|run\)\s*;', 'bnW', stop)
call setpos('.', save)
return p > 0 && p > q
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_PATCH_EOF
# =============================================================================
write_file 'indent/sas.vim' <<'SASIDE_PATCH_EOF'
" =============================================================================
" File: indent/sas.vim
" Description: SAS用スマートインデント(syntax非依存版)
"
" 従来版との違い:
" コメント・文字列の判定を synID() ではなく autoload/sas/code.vim の
" 字句走査に委ねる。これにより syntax が効いていない環境でも
" バナーコメントの中身がコードと誤認されず、後続行が引きずられない。
"
" 対応するブロック:
" DATA/PROC ~ RUN;/QUIT; DO/SELECT ~ END; %DO ~ %END; %MACRO ~ %MEND;
" 括弧の複数行記述(データセットオプション等)
" 継続行(; で終わっていない文の続き)
" 触らない領域:
" ブロックコメントの内側・星コメントの内側・datalines/cards の生データ
" =============================================================================
if exists("b:did_indent")
finish
endif
let b:did_indent = 1
setlocal indentexpr=SasIndent()
setlocal indentkeys=!^F,o,O,0),=~end,=~run,=~quit,=~%end,=~%mend,=~else,=~%else,=~when,=~otherwise
let s:cpo_save = &cpo
set cpo&vim
" =============================================================================
" パターン定義
"
" 注意: ftplugin が 'iskeyword' に % と & を足しているため、\< \> の意味が
" 環境によって変わる。ここでは iskeyword に依存しないよう、
" 単語境界を明示的な文字クラスで表現している。
" =============================================================================
" DATA/PROC の開始。data= オプションや datalines は除外される
let s:P_DATAPROC = '\c^\s*\%(data\|proc\)\%([^A-Za-z0-9_=]\|$\)'
let s:P_RUNQUIT = '\c^\s*\%(run\|quit\)\s*;'
" %do を誤って拾わないよう直前の % を除外
let s:P_DO = '\c\%(^\|[^%A-Za-z0-9_]\)\zsdo\%([^A-Za-z0-9_]\|$\)'
let s:P_END = '\c^\s*end\s*;'
let s:P_PDO = '\c%do\%([^A-Za-z0-9_]\|$\)'
let s:P_PEND = '\c^\s*%end\s*;'
let s:P_MACRO = '\c^\s*%macro\%([^A-Za-z0-9_]\|$\)'
let s:P_MEND = '\c^\s*%mend\%([^A-Za-z0-9_]\|$\)'
" DATAステップの SELECT。SQL の select は select( / select; の形を取らない
let s:P_SELECT = '\c^\s*select\s*[(;]'
" =============================================================================
" ヘルパー
" =============================================================================
" searchpair の skip 式(コメント・文字列内のマッチを無視する)
function! s:Skip() abort
return sas#code#InCommentOrString(line('.'), col('.'))
endfunction
" 直前の「実コードがある行」を返す(コメント行・空行を飛ばす)
function! s:PrevCode(lnum) abort
let l = prevnonblank(a:lnum - 1)
while l > 0
if !sas#code#InComment(l) && sas#code#Text(l) !~# '^\s*$'
return l
endif
let l = prevnonblank(l - 1)
endwhile
return 0
endfunction
" 文が完結しているか(; で終わっているか)
function! s:Terminated(code) abort
return a:code =~# ';\s*$'
endfunction
" 継続中の文が始まった行を探す
function! s:StatementStart(lnum) abort
let l = a:lnum
let guard = 0
while l > 1 && guard < 200
let p = s:PrevCode(l)
if p == 0 || s:Terminated(sas#code#Text(p))
return l
endif
let l = p
let guard += 1
endwhile
return l
endfunction
" パターンの出現回数を数える
function! s:Count(str, pat) abort
let n = 0
let pos = 0
let guard = 0
while guard < 50
let m = match(a:str, a:pat, pos)
if m == -1
break
endif
let n += 1
let e = matchend(a:str, a:pat, pos)
let pos = e > m ? e : m + 1
let guard += 1
endwhile
return n
endfunction
" その行が正味で何段ぶんブロックを開いたか
" (1行完結の if x then do; ... end; を +0 と正しく扱うため、
" 開きと閉じを同一行内で相殺する)
function! s:OpenDelta(code, lnum) abort
let n = 0
if a:code =~# s:P_DATAPROC
let n += 1
endif
if a:code =~# s:P_MACRO
let n += 1
endif
let n += s:Count(a:code, s:P_DO)
let n += s:Count(a:code, s:P_PDO)
" SELECT は DATAステップのときだけブロック扱い(PROC SQL では数えない)
if a:code =~# s:P_SELECT && !sas#code#InProcSql(a:lnum)
let n += 1
endif
" 同一行の閉じで相殺
let n -= s:Count(a:code, '\c\%(^\|[^%A-Za-z0-9_]\)end\s*;')
let n -= s:Count(a:code, s:P_PEND)
let n -= s:Count(a:code, s:P_MEND)
if a:code =~# s:P_RUNQUIT
let n -= 1
endif
return n
endfunction
" else に対応する if の行を探す
function! s:FindIf(from) abort
let l = a:from
let base = indent(a:from)
let guard = 0
while l > 0 && guard < 200
let guard += 1
let code = sas#code#Text(l)
" end; に当たったら対応する do まで飛ぶ
if code =~# s:P_END
call cursor(l, 1)
let m = searchpair(s:P_DO, '', s:P_END, 'bnW', 's:Skip()')
if m > 0 && m < l
let l = m
continue
endif
endif
if code =~# '\c^\s*if\%([^A-Za-z0-9_]\|$\)' && indent(l) <= base
return l
endif
" ブロック境界を越えたら諦める
if code =~# s:P_DATAPROC || code =~# s:P_MACRO
return 0
endif
let l = s:PrevCode(l)
endwhile
return 0
endfunction
" %else に対応する %if の行を探す
function! s:FindPctIf(from) abort
let l = a:from
let guard = 0
while l > 0 && guard < 200
let guard += 1
let code = sas#code#Text(l)
if code =~# s:P_PEND
call cursor(l, 1)
let m = searchpair(s:P_PDO, '', s:P_PEND, 'bnW', 's:Skip()')
if m > 0 && m < l
let l = m
continue
endif
endif
if code =~# '\c^\s*%if\%([^A-Za-z0-9_]\|$\)'
return l
endif
if code =~# s:P_MACRO
return 0
endif
let l = s:PrevCode(l)
endwhile
return 0
endfunction
" =============================================================================
" 本体
" =============================================================================
function! SasIndent() abort
let lnum = v:lnum
" ---------------------------------------------------------------
" 0) 触ってはいけない領域
" ---------------------------------------------------------------
" datalines/cards の生データは桁位置に意味があるので絶対に変えない
if sas#code#InDataLines(lnum)
return -1
endif
" ブロックコメント・星コメントの内側は書き手の体裁を尊重する
" (/*===== ... =====*/ のバナーが崩れるのを防ぐ)
if sas#code#InComment(lnum)
return -1
endif
let pnum = s:PrevCode(lnum)
if pnum == 0
return 0
endif
let cline = sas#code#Text(lnum)
let pline = sas#code#Text(pnum)
" ---------------------------------------------------------------
" 1) 括弧の内側(データセットオプションの複数行記述など)
" 行頭が ) → 開き括弧の行に揃える
" 名前= の形 → 開き括弧の行 +1段
" それ以外(値) → 開き括弧の行 +2段
" ---------------------------------------------------------------
call cursor(lnum, 1)
let open = searchpairpos('(', '', ')', 'bnW', 's:Skip()', max([1, lnum - 500]))
if open[0] > 0
if cline =~# '^\s*)'
return indent(open[0])
elseif cline =~# '^\s*[A-Za-z_&%][A-Za-z0-9_&.%]*\s*='
return indent(open[0]) + shiftwidth()
else
return indent(open[0]) + 2 * shiftwidth()
endif
endif
" ---------------------------------------------------------------
" 2) 現在行がブロックの閉じ → 対応する開始行に揃える
" ---------------------------------------------------------------
if cline =~# s:P_RUNQUIT
call cursor(lnum, 1)
let m = searchpair(s:P_DATAPROC, '', s:P_RUNQUIT, 'bnW', 's:Skip()')
return m > 0 ? indent(m) : 0
endif
if cline =~# s:P_PEND
call cursor(lnum, 1)
let m = searchpair(s:P_PDO, '', s:P_PEND, 'bnW', 's:Skip()')
return m > 0 ? indent(m) : indent(pnum)
endif
if cline =~# s:P_MEND
call cursor(lnum, 1)
let m = searchpair(s:P_MACRO, '', s:P_MEND, 'bnW', 's:Skip()')
return m > 0 ? indent(m) : 0
endif
if cline =~# s:P_END
" PROC SQL の CASE 式の end は閉じではない
if !sas#code#InProcSql(lnum)
call cursor(lnum, 1)
let m = searchpair(s:P_DO, '', s:P_END, 'bnW', 's:Skip()')
return m > 0 ? indent(m) : indent(pnum)
endif
endif
" ---------------------------------------------------------------
" 3) else / %else は対応する if / %if に揃える
" ---------------------------------------------------------------
if cline =~# '\c^\s*else\%([^A-Za-z0-9_]\|$\)'
let m = s:FindIf(pnum)
return m > 0 ? indent(m) : indent(pnum)
endif
if cline =~# '\c^\s*%else\%([^A-Za-z0-9_]\|$\)'
let m = s:FindPctIf(pnum)
return m > 0 ? indent(m) : indent(pnum)
endif
" ---------------------------------------------------------------
" 4) 直前行が文を終えていない(継続行)→ 文の開始行から1段深く
" ---------------------------------------------------------------
if !s:Terminated(pline)
return indent(s:StatementStart(pnum)) + shiftwidth()
endif
" ---------------------------------------------------------------
" 5) 直前行が開いたブロックの段数だけ深くする
" ---------------------------------------------------------------
let delta = s:OpenDelta(pline, pnum)
let ind = indent(pnum)
if delta > 0
let ind += delta * shiftwidth()
endif
return ind
endfunction
" =============================================================================
" デバッグ用: カーソル行の判定結果を表示する
" インデントがおかしいと感じたら :SasIndentInfo で内訳を確認できる
" =============================================================================
function! s:IndentInfo() abort
let l = line('.')
let p = s:PrevCode(l)
echo printf("行 : %d", l)
echo printf("コメント内 : %s", sas#code#InComment(l) ? 'YES(インデント変更しない)' : 'no')
echo printf("datalines内 : %s", sas#code#InDataLines(l) ? 'YES(インデント変更しない)' : 'no')
echo printf("PROC SQL内 : %s", sas#code#InProcSql(l) ? 'yes' : 'no')
echo printf("コード部分 : [%s]", sas#code#Text(l))
echo printf("直前のコード行: %d [%s]", p, p > 0 ? sas#code#Text(p) : '')
if p > 0
echo printf("直前行の終端 : %s", s:Terminated(sas#code#Text(p)) ? '; で完結' : '継続中')
echo printf("直前行の開き : %+d 段", s:OpenDelta(sas#code#Text(p), p))
endif
echo printf("算出インデント: %d (現在 %d)", SasIndent(), indent(l))
endfunction
command! -buffer SasIndentInfo call <SID>IndentInfo()
let &cpo = s:cpo_save
unlet s:cpo_save
let b:undo_indent = "setlocal indentexpr< indentkeys< | silent! delcommand SasIndentInfo"
SASIDE_PATCH_EOF
# =============================================================================
write_file 'autoload/saside/format.vim' <<'SASIDE_PATCH_EOF'
" =============================================================================
" File: autoload/saside/format.vim
" Module: format — コード整形(syntax非依存版)
" Description: 1) indent/sas.vim による全体再インデント
" 2) 行末空白の除去
" 3) 3行以上の連続空行を1行へ圧縮(コメント内は保護)
" 4) g:saside_format_upcase=1 のとき予約語を大文字化
" (文字列/コメント内は sas#code の字句走査で保護する)
" 全処理は1回のundoで戻せる。
"
" 従来版との違い:
" 大文字化の保護判定を synID() ではなく sas#code#InCommentOrString() に
" 変更した。syntax が効いていない環境では従来版は文字列やコメントの中身
" まで大文字化してしまい、データ値やラベルが壊れる恐れがあった。
" =============================================================================
let s:cpo_save = &cpo
set cpo&vim
function! saside#format#Format() abort
let view = winsaveview()
try
" 1) 再インデント(コメント内・datalines内は indent 側が -1 を返すので保持)
normal! gg=G
" 2) 行末空白の除去
keepjumps keeppatterns silent! %s/\s\+$//e
" 3) 連続空行の圧縮(コメントの中は触らない)
call s:squeeze_blank_lines()
" 4) 予約語の大文字化(任意)
if get(g:, 'saside_format_upcase', 0)
call s:upcase_buffer()
endif
finally
call winrestview(view)
endtry
call saside#util#Info('整形しました')
endfunction
" 3行以上の連続空行を1行にする。ただしコメントブロックの内側は保持する
" (バナーコメント内の意図的な余白を潰さないため)
function! s:squeeze_blank_lines() abort
let lnum = line('$')
while lnum > 1
if getline(lnum) =~# '^\s*$'
let start = lnum
while start > 1 && getline(start - 1) =~# '^\s*$'
let start -= 1
endwhile
let n = lnum - start + 1
if n >= 3 && !sas#code#InComment(start)
execute (start + 1) . ',' . lnum . 'delete _'
endif
let lnum = start - 1
else
let lnum -= 1
endif
endwhile
endfunction
" 予約語(ステートメント語)を大文字化する。
" 文字列・コメントの中はテキスト走査で判定して保護する。
function! s:upcase_buffer() abort
let words = saside#parser#Keywords().statement
let pat = '\c\%(^\|[^A-Za-z0-9_&%]\)\zs\%(' . join(words, '\|')
\ . '\)\%([^A-Za-z0-9_]\|$\)\@='
for lnum in range(1, line('$'))
" コメント行・datalines行はまるごとスキップ
if sas#code#InComment(lnum) || sas#code#InDataLines(lnum)
continue
endif
let line = getline(lnum)
let out = ''
let pos = 0
let guard = 0
while guard < 200
let guard += 1
let [str, mstart, mend] = matchstrpos(line, pat, pos)
if mstart == -1
let out .= strpart(line, pos)
break
endif
let out .= strpart(line, pos, mstart - pos)
if sas#code#InCommentOrString(lnum, mstart + 1)
let out .= str
else
let out .= toupper(str)
endif
let pos = mend
endwhile
if out !=# line
call setline(lnum, out)
endif
endfor
endfunction
let &cpo = s:cpo_save
unlet s:cpo_save
SASIDE_PATCH_EOF
# =============================================================================
write_file 'indent.test.sas' <<'SASIDE_PATCH_EOF'
/********************************************
インデント検証用サンプル
このファイルを開いて gg=G (または F9)を実行し、
以下の体裁が崩れないことを確認する
********************************************/
/*==========================================
Purpose : バナーコメントの中身が
後続コードを引きずらないこと
Author : test
==========================================*/
libname raw '/data/raw';
/* 1) DATAステップ基本 */
data work.a;
set raw.b;
where amount > 0;
if flag = 1 then do;
x = 1;
y = 2;
end;
else do;
x = 0;
end;
run;
/* 2) 1行完結の do/end は段がずれないこと */
data work.a2;
set raw.b;
if x then do; y = 1; end;
z = 3;
run;
/* 3) PROC SQL の select がブロック扱いされないこと */
proc sql;
create table work.c as
select id,
sum(amount) as amt
from work.a
group by id;
select count(*) from work.a;
create table work.d as
select a.*,
case when a.x > 0 then 1
else 0
end as flg
from work.a as a;
quit;
/* 4) 括弧の複数行記述 */
data work.e;
set raw.b(
keep =
id
amount
rename = (
amount = amt
)
);
run;
/* 5) 文字列の中の /* や ( に引きずられないこと */
data work.f;
set work.e;
memo = 'コメント記号 /* と 括弧 ( を含む文字列';
note = "閉じ括弧 ) も含む";
run;
/* 6) data= オプションが DATAステップ開始と誤認されないこと */
proc means data=work.a n mean;
var amt;
run;
/* 7) マクロ */
%macro run_month(from=, to=);
%do i = &from. %to &to.;
%if &i. = 1 %then %do;
%put NOTE: first;
%end;
%else %do;
%put NOTE: &i.;
%end;
%end;
%mend run_month;
/* 8) SELECT-WHEN(DATAステップ) */
data work.g;
set work.a;
select (x);
when (1) y = 'a';
when (2) y = 'b';
otherwise y = 'z';
end;
run;
/* 9) datalines の生データが整形されないこと */
data work.h;
input id name $ amount;
datalines;
1 aaa 100
2 bbb 200
3 ccc 300
;
run;
* 10) 星コメントも
複数行にわたって保護されること;
data work.i;
set work.h;
run;
SASIDE_PATCH_EOF
echo ""
echo "完了: $count ファイルを更新しました"
[ -d "$BACKUP" ] && echo "変更前ファイルの退避先: $BACKUP"
echo ""
echo "次の手順:"
echo " 1. vim を再起動"
echo " 2. vim ~/.vim/indent.test.sas を開く"
echo " 3. gg=G (または F9)を実行し、体裁が崩れないことを確認"
echo " 4. 崩れる箇所があれば、その行で :SasIndentInfo を実行して"
echo " 判定内訳(コメント内か・直前コード行・開き段数)を確認する"
echo ""
echo "備考:"
echo " インデント判定は syntax に依存しなくなりました。"
echo " 色が出ていない環境でもインデントは正しく動作します。"