Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ python3 scripts/gen_testdata.py --encoding-set --out-dir testdata/ # UTF-8 / S
python3 scripts/gen_testdata.py --size 10G --jp --out testdata/test_10gb.log
```

配布用ディスクイメージ(`.build/MrEditor-1.18.0.dmg`)の作成:
配布用ディスクイメージ(`.build/MrEditor-1.19.0.dmg`)の作成:

```sh
sh scripts/make_dmg.sh
Expand Down Expand Up @@ -362,7 +362,8 @@ vmmap $(pgrep -x MrEditor) | grep test_10gb.log # → 10.0G 2.8G 0K (vsiz
- **1.15.0 — 日本語の CSV が重かった。** 日本語の CSV は Shift-JIS・CRLF・引用符つきで届く。その検索が **1 行ずつ `String` へ変換してから照合していて、1.06GB・581万行で 26.2 秒**かかっていた。release ビルドにしても縮まない —— 自分のコードではなく、581万回の文字列生成そのものが費用だったから。**探す語のほうを Shift-JIS のバイト列にして、直接走査するようにした。2.4 秒(11 倍)。**2 バイト目が ASCII と重なるので、当たった位置が文字の切れ目に乗っているかを確かめて誤爆を弾く。あわせて **検索バーが「終わった」を出す** ようになった —— 走査中は件数と進み具合が一緒に動き、終わると進み具合が消える。前は 1 件当たった瞬間に件数だけになり、まだ増え続けるのに確定したように見えていた ✅(1.15.0)
- **1.16.0 — 閉じるタブを1つ間違えると、戻り道はファイル ▸ 開く…でパスを記憶から打ち直すことだけだった。** **`⇧⌘T` で、いま閉じたファイルを開き直せる**ようにした。閉じたドキュメントはスタックで持ち(直近が末尾)、押すたびに1件 pop して開く——押し続ければどんどん前まで戻れる。保存先が確定していない新規ドキュメント(まだ「名称未設定」のまま)は積まない。ディスク上に開き直す先が無いため ✅(1.16.0)
- **1.17.0 — 新しくコピーすると、直前のコピーは消える。** 貼り付け先を間違えたとき、戻る方法がコピー元のアプリを開き直す以外に無かった。**編集メニュー→クリップボード履歴**(ツールバーにも任意で追加できる)に直近のコピーを並べ、選べばそのまま貼り付く。**ディスクには残さず**、1Password 等が付ける「見せない」の印(`org.nspasteboard.ConcealedType`)が立ったコピーは記録しない ✅(1.17.0)
- **1.18.0 — AI診断は「鍵は自分の Mac から出さない」と約束したが、鍵そのものを外に置きたくない人には、まだクラウドの契約が要った。** **BYOK にローカルの Ollama を追加**(環境設定 > AI)——選べば鍵は不要で、リクエストも機外に出ない。あわせて検索バーに**「最近の検索」**(過去に検索した語をメニューから選び直せる)と、**Markdown/主要言語の構文ハイライト**(見出し・強調・コード・リンク、コメント・文字列・数値・予約語。対応: Python/Swift/JS/Shell/YAML/JSON/SQL/CSS/Java/PHP/Go/Ruby/Perl)を追加。一致行だけ表示(フィルタ)中に検索バーを開き直すと置換ボタンが押せる見た目のまま実は効かない不具合も修正——読み取り専用バナーに理由を出すようにした ✅(このリリース)
- **1.18.0 — AI診断は「鍵は自分の Mac から出さない」と約束したが、鍵そのものを外に置きたくない人には、まだクラウドの契約が要った。** **BYOK にローカルの Ollama を追加**(環境設定 > AI)——選べば鍵は不要で、リクエストも機外に出ない。あわせて検索バーに**「最近の検索」**(過去に検索した語をメニューから選び直せる)と、**Markdown/主要言語の構文ハイライト**(見出し・強調・コード・リンク、コメント・文字列・数値・予約語。対応: Python/Swift/JS/Shell/YAML/JSON/SQL/CSS/Java/PHP/Go/Ruby/Perl)を追加。一致行だけ表示(フィルタ)中に検索バーを開き直すと置換ボタンが押せる見た目のまま実は効かない不具合も修正——読み取り専用バナーに理由を出すようにした ✅(1.18.0)
- **1.19.0 — ターミナルやAIの回答からコピーしたパスは、File ▸ Openで手打ちし直す以外に開く方法が無かった。** **ファイル ▸ パスを指定して開く…(⌥⌘O)**は貼り付けたパスをそのまま受け取る——前後の引用符や`~`を処理し、ファイルはエディタで開く。フォルダはMrEditor内に行き先が無いのでFinderで表示するだけにした——独自の閲覧UIは作っていない ✅(このリリース)
- **以降** — その他の分析ツール

> **⚠️ v0.7 以前の dmg は、ダウンロードした Mac で起動しません。**
Expand All @@ -388,8 +389,10 @@ vmmap $(pgrep -x MrEditor) | grep test_10gb.log # → 10.0G 2.8G 0K (vsiz
| フォルダを横断して検索 | 結果をクリックするとその行が開く。Shift-JIS / EUC-JP も判定して読む | 10GB・リテラル 1 語で **3.14 秒**(ripgrep と互角) |

無料版にも「分析」メニューは同じ位置にあります(グレーアウトはしません)。押すと、
その機能の説明が 1 枚出ます。MrkEditor は Polar で販売しています:
https://buy.polar.sh/polar_cl_S32h7CYTXCxbYQ5IimcpI4DayCFkHUYQ8I6bv2tOki8
その機能の説明が 1 枚出ます。専用ページで詳しく読めます。購入は Polar からです:

- https://mr-tabata.github.io/MrEditor/MrkEditor.ja.html
- https://buy.polar.sh/polar_cl_S32h7CYTXCxbYQ5IimcpI4DayCFkHUYQ8I6bv2tOki8

## まだ「作らない」もの

Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ python3 scripts/gen_testdata.py --encoding-set --out-dir testdata/ # UTF-8 / S
python3 scripts/gen_testdata.py --size 10G --jp --out testdata/test_10gb.log
```

Build a distributable disk image (`.build/MrEditor-1.18.0.dmg`):
Build a distributable disk image (`.build/MrEditor-1.19.0.dmg`):

```sh
sh scripts/make_dmg.sh
Expand Down Expand Up @@ -387,7 +387,8 @@ vmmap $(pgrep -x MrEditor) | grep test_10gb.log # → 10.0G 9.1G 0K (vsiz
- **1.15.0 — Japanese CSVs were slow.** They arrive as Shift-JIS with CRLF and quoted fields, and searching one **decoded every line into a `String` before matching — 26.2s on a 1.06 GB, 5.8M-line file**. A release build did not help: the cost was 5.8M string allocations, not our own code. **Now the search term is encoded into Shift-JIS and the bytes are scanned directly. 2.4s — 11x.** A trailing byte can look like ASCII, so a match only counts when it lands on a character boundary. **The search bar also tells you when it is done**: the count and the percentage move together while scanning, and the percentage disappears at the end. Before, the count replaced "searching" as soon as the first match landed, so it looked settled while it was still growing ✅ (1.15.0)
- **1.16.0 — Close the wrong tab a moment too soon, and the only way back was File ▸ Open, typing the path from memory.** **`⇧⌘T` reopens the file you just closed.** Closed documents are kept on a stack, most recent last; each press pops one and opens it, going further back the more you press. A document still untitled (never saved to a path) is not pushed — there is nothing on disk to reopen ✅ (1.16.0)
- **1.17.0 — Copy something new, and the one before it is gone.** Paste in the wrong place and the only way back was switching to whatever app you copied it from and doing it again. **Edit ▸ Clipboard History** (and an optional toolbar button) lists recent copies — pick one and it pastes right there. **Nothing is written to disk**, and anything a password manager marks concealed (`org.nspasteboard.ConcealedType`) is never recorded ✅ (1.17.0)
- **1.18.0 — AI diagnose already promised the key never leaves your Mac, but anyone unwilling to hand a key to the cloud at all still had nowhere to point it.** **BYOK now includes local Ollama** (Preferences ▸ AI) — pick it and no key, and no request, ever leaves the machine. Also added: **recent searches** in the search bar (pick a past query from a menu), and **Markdown/major-language syntax highlighting** (headings, emphasis, code, links; comments, strings, numbers, keywords — Python/Swift/JS/Shell/YAML/JSON/SQL/CSS/Java/PHP/Go/Ruby/Perl). Fixed a bug where reopening the search bar while filtering left Replace looking enabled when it silently did nothing — the read-only banner now says why ✅ (this release)
- **1.18.0 — AI diagnose already promised the key never leaves your Mac, but anyone unwilling to hand a key to the cloud at all still had nowhere to point it.** **BYOK now includes local Ollama** (Preferences ▸ AI) — pick it and no key, and no request, ever leaves the machine. Also added: **recent searches** in the search bar (pick a past query from a menu), and **Markdown/major-language syntax highlighting** (headings, emphasis, code, links; comments, strings, numbers, keywords — Python/Swift/JS/Shell/YAML/JSON/SQL/CSS/Java/PHP/Go/Ruby/Perl). Fixed a bug where reopening the search bar while filtering left Replace looking enabled when it silently did nothing — the read-only banner now says why ✅ (1.18.0)
- **1.19.0 — A path copied from a terminal or an AI's answer had nowhere to go but File ▸ Open, retyped by hand.** **File ▸ Open by Path… (⌥⌘O)** takes a pasted path directly — surrounding quotes and `~` are handled — and opens a file in the editor. A folder has nowhere to go inside MrEditor, so it is revealed in Finder instead; no path-browsing UI was built for it ✅ (this release)
- **later** — more analysis tooling

> **⚠️ Builds up to v0.7 do not launch on a Mac that downloaded them.**
Expand All @@ -413,8 +414,10 @@ answer out of it is paid.**
| Search across folders | click a hit and the file opens at that line; Shift-JIS / EUC-JP detected per file | **3.14 s** for one literal over a single 10 GB log (on par with ripgrep) |

The free app has the same Analyze menu in the same place (never greyed out); choosing an item
shows one page explaining that feature. MrkEditor is sold through Polar:
https://buy.polar.sh/polar_cl_S32h7CYTXCxbYQ5IimcpI4DayCFkHUYQ8I6bv2tOki8
shows one page explaining that feature. See the dedicated MrkEditor page, or buy through Polar:

- https://mr-tabata.github.io/MrEditor/MrkEditor.html
- https://buy.polar.sh/polar_cl_S32h7CYTXCxbYQ5IimcpI4DayCFkHUYQ8I6bv2tOki8

## Not yet

Expand Down
17 changes: 17 additions & 0 deletions Sources/MrEditorCore/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenu
/// 開いた遠隔の面。持っておかないと即座に閉じる(NSWindowController は自分を保持しない)。
private var remoteWindows: [RemoteWindowController] = []
private var preferencesController: PreferencesWindowController?
private var pathOpenController: PathOpenWindowController?

func applicationDidFinishLaunching(_ notification: Notification) {
// 開発ビルド(バンドル無し)でも Dock・About でアプリアイコンを出す。
Expand Down Expand Up @@ -342,6 +343,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenu
controller.window?.makeKeyAndOrderFront(nil)
}

/// ターミナルや AI の回答からコピーしたパスを貼り付けて開く(⌥⌘O)。
@objc private func openByPath(_ sender: Any?) {
if pathOpenController == nil {
pathOpenController = PathOpenWindowController(openHandler: { [weak self] url in
self?.ensureController().open(url: url)
})
}
pathOpenController?.show()
}

/// File > 最近使った項目/Edit > クリップボード履歴、サブメニューを開くたびに再構築する。
func menuNeedsUpdate(_ menu: NSMenu) {
if menu === recentMenu { updateRecentMenu(menu); return }
Expand Down Expand Up @@ -607,6 +618,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate, NSMenu
remoteItem.keyEquivalentModifierMask = [.command, .control]
remoteItem.target = self
fileMenu.addItem(remoteItem)
// ターミナルや AI の回答からコピーしたパスを貼り付けて開く。
let pathItem = NSMenuItem(title: L("menu.openByPath"),
action: #selector(openByPath(_:)), keyEquivalent: "o")
pathItem.keyEquivalentModifierMask = [.command, .option]
pathItem.target = self
fileMenu.addItem(pathItem)
// 最近使った項目(サブメニューは開くたびに menuNeedsUpdate で再構築)
let recentItem = NSMenuItem(title: L("menu.openRecent"), action: nil, keyEquivalent: "")
let recent = NSMenu(title: L("menu.openRecent"))
Expand Down
2 changes: 1 addition & 1 deletion Sources/MrEditorCore/AppInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ enum AppInfo {
static var version: String {
(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? fallbackVersion
}
private static let fallbackVersion = "1.18.0"
private static let fallbackVersion = "1.19.0"

/// ヘルプメニューから開くプロジェクトページ。
static let helpURL = URL(string: "https://github.com/MR-TABATA/MrEditor")!
Expand Down
8 changes: 8 additions & 0 deletions Sources/MrEditorCore/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@
"prefs.share.copied" = "Share link copied to clipboard.";
"prefs.share.imported" = "Settings applied.";
"prefs.share.export.defaultName" = "MrEditor Theme";

/* Open by path */
"pathOpen.title" = "Open by Path";
"pathOpen.placeholder" = "Path to a file or folder";
"pathOpen.open" = "Open";
"pathOpen.notFound" = "That path could not be found";
"share.apply.title" = "Apply these appearance settings?";
"share.apply.confirm" = "Apply";
"share.import.failed.title" = "Couldn’t read the settings";
Expand Down Expand Up @@ -384,6 +390,8 @@
"menu.nextBookmark" = "Next Bookmark";
"menu.prevBookmark" = "Previous Bookmark";

"menu.openByPath" = "Open by Path…";

/* --- Remote (B5) --- */
"menu.openRemote" = "Open Remote…";
"remote.title" = "Remote";
Expand Down
8 changes: 8 additions & 0 deletions Sources/MrEditorCore/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@
"prefs.share.copied" = "共有リンクをクリップボードにコピーしました。";
"prefs.share.imported" = "設定を適用しました。";
"prefs.share.export.defaultName" = "MrEditor テーマ";

/* パスを指定して開く */
"pathOpen.title" = "パスを指定して開く";
"pathOpen.placeholder" = "ファイルまたはフォルダのパス";
"pathOpen.open" = "開く";
"pathOpen.notFound" = "そのパスが見つかりません";
"share.apply.title" = "この外観設定を適用しますか?";
"share.apply.confirm" = "適用";
"share.import.failed.title" = "設定を読み込めませんでした";
Expand Down Expand Up @@ -384,6 +390,8 @@
"menu.nextBookmark" = "次のしおりへ";
"menu.prevBookmark" = "前のしおりへ";

"menu.openByPath" = "パスを指定して開く…";

/* --- リモート(B5) --- */
"menu.openRemote" = "リモートを開く…";
"remote.title" = "リモート";
Expand Down
105 changes: 105 additions & 0 deletions Sources/MrEditorCore/UI/PathOpenWindowController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import AppKit

/// 「パスを指定して開く…」(⌥⌘O)。ターミナルや AI の回答からコピーしたローカルパスを
/// 貼り付けて開くための小さな窓。狙いは独自性ではなく、パスを迷わず開ける使いやすさ。
///
/// **フォルダを開く仕組みは MrEditor に無い**(サイドバーのファイルツリー等を持たない)。
/// フォルダが渡されたら、独自の閲覧 UI は作らず Finder で見せるだけにする。
final class PathOpenWindowController: NSWindowController {

private let pathField = NSTextField()
private let errorLabel = NSTextField(labelWithString: "")
private let openHandler: (URL) -> Void

init(openHandler: @escaping (URL) -> Void) {
self.openHandler = openHandler
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 110),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.title = L("pathOpen.title")
window.isReleasedWhenClosed = false
super.init(window: window)
window.center()
buildLayout()
}

required init?(coder: NSCoder) { fatalError("init(coder:) は使わない") }

/// ウィンドウを最前面に出す(無ければ生成済みのものを再利用)。呼ぶたびに入力欄を空にする。
func show() {
pathField.stringValue = ""
errorLabel.stringValue = ""
showWindow(nil)
window?.makeKeyAndOrderFront(nil)
window?.makeFirstResponder(pathField)
NSApp.activate(ignoringOtherApps: true)
}

private func buildLayout() {
guard let content = window?.contentView else { return }

pathField.placeholderString = L("pathOpen.placeholder")
pathField.target = self
pathField.action = #selector(openTapped)
pathField.translatesAutoresizingMaskIntoConstraints = false
pathField.widthAnchor.constraint(greaterThanOrEqualToConstant: 340).isActive = true

let openButton = NSButton(title: L("pathOpen.open"), target: self, action: #selector(openTapped))
openButton.keyEquivalent = "\r"

errorLabel.textColor = .systemRed
errorLabel.font = .systemFont(ofSize: 11)

let row = NSStackView(views: [pathField, openButton])
row.orientation = .horizontal
row.spacing = 8

let stack = NSStackView(views: [row, errorLabel])
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 6
stack.translatesAutoresizingMaskIntoConstraints = false
content.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
stack.trailingAnchor.constraint(lessThanOrEqualTo: content.trailingAnchor, constant: -20),
stack.topAnchor.constraint(equalTo: content.topAnchor, constant: 24),
])
}

@objc private func openTapped() {
guard let url = Self.resolve(pathField.stringValue) else {
errorLabel.stringValue = L("pathOpen.notFound")
return
}
var isDir: ObjCBool = false
FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir)
if isDir.boolValue {
// 開く先が無いので、Finder で見せるところまでで済ませる。
NSWorkspace.shared.activateFileViewerSelecting([url])
} else {
openHandler(url)
}
window?.performClose(nil)
}

/// 入力文字列を実在するファイル/フォルダの URL にする。無ければ nil。
/// 前後の空白と、コピー時に付く外側の引用符(1 組だけ)を落とし、`~` を展開する。
/// 相対パスは対応しない(GUI アプリの cwd は実用上 `/` になるだけで、基準にならない)。
static func resolve(_ raw: String) -> URL? {
var s = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if s.count >= 2 {
let pairs: [(Character, Character)] = [("\"", "\""), ("'", "'"), ("\u{201C}", "\u{201D}")]
for (open, close) in pairs where s.first == open && s.last == close {
s = String(s.dropFirst().dropLast())
break
}
}
s = (s as NSString).expandingTildeInPath
guard !s.isEmpty, FileManager.default.fileExists(atPath: s) else { return nil }
return URL(fileURLWithPath: s)
}
}
Loading
Loading