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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ media/ quickNote.css/js, noteroot.svg (Activity Bar), icon.png, walk
- **Move to ▸** (submenu `noteRoot.moveToMenu`, label "NoteRoot: Move to", declared in `contributes.submenus`; it is a **static** submenu with two fixed commands, because VS Code cannot put a changing list into a menu): `noteRoot.moveToQuickNote` ("Quick Note", `QuickNoteService.append`) and `noteRoot.moveTo` ("Choose Note…", the list below); both have the category "NoteRoot: Move to" so the Command Palette reads "NoteRoot: Move to: …", and both go through `cutSelectionTo(chooseDestination)` in `fileCommands.ts`. `noteRoot.moveTo` is cut and paste of the **selected text** of the active editor into another note (not a move of the file). It needs a non-empty selection in a file inside the notes folder (`pathGuard.isInsideRoot`, else an information message from `Strings.moveSelection`), asks for the destination with `pickDestination` in the same file: one QuickPick with **New note…** on top (then an input box checked by `QuickNoteService.validateTargetPath`, `normalizeTargetPath`) the notes of the Recent panel that still exist as files of the list (`RecentNotesService.list()`, newest first, under a separator "Recent"; `recentService` is a dependency of `registerFileCommands`) and then every other note file of the notes folder under "All notes" (`SearchService.walkFiles`, `isNoteFile` with the folder's `noteFileTypes`, the note being cut from left out, at most `MAX_MOVE_TARGETS` = 2000, sorted naturally; label = file name, description = folder, `matchOnDescription`), no open dialog; `chooseWithPreview` builds it with `createQuickPick` and follows `onDidChangeActive`: the highlighted note is shown with `showTextDocument(uri, { viewColumn: Beside, preview: true, preserveFocus: true, selection: end of the file })` (not for a file bigger than `maxTextFileBytes`; a newer highlight cancels an older one with a ticket), and at the end the tabs this command opened are closed with `closeTabsOf` (a tab that was open before stays); the mouse pointer alone does not change the highlight in a QuickPick, so the preview follows the keyboard and clicks, not a plain hover; it refuses the note it is cut from, then `QuickNoteService.appendText(relativePath, text)`: queued with the Quick Notes, writes the text **as it is** at the end (a blank line before it, no heading, no tag line, the file's own line ending, the file is created if new; an open editor gets a `WorkspaceEdit` and a clean file is saved again, like Quick Notes). Several selections are joined with a blank line. **The text is removed from the source only after the append worked** and only if the source did not change meanwhile (`version` equal, or every range still holds the same text), with a `WorkspaceEdit` that Ctrl+Z undoes; otherwise it stays and `moveSelection.changedMeanwhile` explains. Menus: `editor/context` with `noteRoot.activeInRoot && editorHasSelection && !editorReadonly`, and the Command Palette with `noteRoot.activeInRoot && editorHasSelection`; `ActiveNoteService` keeps the key `noteRoot.activeInRoot` (true while the active tab is a file inside the root that the exclude patterns do not hide; updated on active editor, tab, root, state and exclude changes). (A first version moved the whole file; that was a misunderstanding and was replaced.)
- **Note header** (`noteRoot.noteHeader` is **folder-only**: `custom.folderSettings.noteHeader`, a list of lines, `isValidHeader`: at most `MAX_HEADER_LINES` 20 lines of at most 200 characters, no line breaks; the default is the comment `<!--` / ` **Summary:** ` / ` **Comment:** ` / `-->`, an empty list means no header; a `list` row in group Notes with `allowDuplicates` and `normalizeEntry` that keeps the spaces): pure `NoteHeader.ts` (`headerBlock`, `headerFields` = the names before the colon of each line, `hasHeader` = the file starts, after BOM and front matter, with an HTML comment that has every field name, whatever is written after them; a template without fields is compared line by line, `planHeader(existing, lines, eol)` = `{ offset, text }` or undefined: the block, then one blank line before old content, after BOM and front matter but **before** any other comment). `startOfNote` moved to `NoteStart.ts` (re-exported by `QuickNoteService`) and now also skips a leading `<!-- ... -->` block (returns it as `comment`; `skipComment = false` stops before it), so `planTagLine`, `findTopTagLine` and `buildStartInsertion` put tags and Quick Notes **below** the header. Writing: `FileOperationsService.createFile` writes the header block into every new file (line ending from `files.eol`, `util/lineEnding.ts`); `QuickNoteService.appendToFile` and `appendToOpenDocument` start a **new or empty** file with it before the first note (so also `appendText` of Move to); `NoteHeaderService.insertHeader(uri)` (`added`, `present`, `skipped`, `off`) is the command for existing notes: `noteRoot.insertHeader` (tree context menu of files, group `2_open@6`, hidden from the palette) and `noteRoot.insertHeaderCurrentNote` (palette), in `headerCommands.ts`; it edits an open document with a `WorkspaceEdit` (saved again only if it was clean, `InternalSaves`) and otherwise writes with `workspace.fs`. Existing files are never changed by themselves.
- **Add ▸** (submenu `noteRoot.addMenu`, label "NoteRoot: Add", in `editor/context` with `noteRoot.activeInRoot && !editorReadonly`; static, two commands with the category "NoteRoot: Add", so the palette reads "NoteRoot: Add: …"): `noteRoot.insertHeaderCurrentNote` ("Header", the header of the **Note header** setting at the top of the active note, `NoteHeaderService.insertHeader`) and `noteRoot.addDateAndTags` ("Date and Tags", `headerCommands.ts`): `buildEntryHead` (exported from `QuickNoteService.ts`, the part of a Quick Note before its text: heading with `quickNoteTimestampFormat` and `quickNoteHeadingLevel`, blank line, tag line from `quickNoteTagLine` with **no tags**, i.e. empty when the template has text of its own, blank line; `buildBlock` uses it too) is inserted by `planEntryInsertion(lineIsBlank, isLastLine, head, eol)` from `NoteHeader.ts`: at the start of an empty line, or at the end of a line with text after a blank line (before more text the block loses its last line ending, so one blank line follows it); the cursor ends right after the block. With no date format and a tag line template that is only `{tags}` there is nothing to add and a message says so. Nothing is saved: it is an editor change that Ctrl+Z undoes.
- **Encryption** (`NoteCrypto.ts` pure: `encryptBytes` / `decryptToBytes`, AES-256-GCM, scrypt N=2^15 r=8 p=1, salt 16 / iv 12 / tag 16, header line `NOTEROOT-ENCRYPTED v1` as AAD, base64 lines of 76; key normalized NFC, `MIN_KEY_LENGTH` 8; wrong key and tampering are both `WrongKeyError`). `NoteEncryptionService` (no UI): `encrypt` refuses `.enc` names, the Quick Note file, unsaved open documents, binary or over 10 MB files and a taken `<name>.enc`; it decrypts its own result and compares, writes it into the original file, reads it back, then renames to `<name>.enc` with `FileOperationsService.renameKeepingEditors` (now public; pins and recents follow). `decryptPermanently` renames first, then writes the plaintext (crash-safe order). `decryptForViewing` returns the text; `DecryptedViewService` shows it in a read-only `noteroot-decrypted:` document (content provider, text dropped on `onDidCloseTextDocument`). Commands in `encryptionCommands.ts` (`encryptNote` asks the key twice with `password: true`, `decryptView`, `decryptNote`; several files share a key). `NoteItem` adds the context suffix `.locked`, a `lock` icon and the click command `decryptView` for `.enc` files; menus group `7_encrypt`, and Run Tagging / Insert Header are hidden for `.locked`. Guards: `SearchService.readText` returns undefined for `.enc` (so search, tag scan, tagging and headers skip them), `NoteTaggingService` skips them, `QuickNoteService.validateTargetPath` / `writeText` refuse them. A link (symbolic) is refused, because a write would go through it to a file that may be outside the root. `decryptPermanently` gives the file its `.enc` name back if writing the plain text fails. `openFile` (every way of opening a file from the tree, search, Recent and Pinned) sends `.enc` files to `noteRoot.decryptView` instead of the editor. `tools/decrypt.js` (not shipped in the `.vsix`) decrypts a note with Node alone and `tools/decrypt.ps1` does the same in Windows PowerShell 5.1 or 7 with nothing installed (scrypt and AES-GCM are written in C# inside the script, because .NET Framework has neither); both must stay in step with `NoteCrypto.ts` (same layout, scrypt parameters and AAD). The key is never stored or logged.
- **Encryption** (`NoteCrypto.ts` pure: `encryptBytes` / `decryptToBytes`, AES-256-GCM, scrypt N=2^15 r=8 p=1, salt 16 / iv 12 / tag 16, header line `NOTEROOT-ENCRYPTED v1` as AAD, base64 lines of 76; key normalized NFC, `MIN_KEY_LENGTH` 8; wrong key and tampering are both `WrongKeyError`). `NoteEncryptionService` (no UI): `encrypt` refuses `.enc` names, the Quick Note file, unsaved open documents, binary or over 10 MB files and a taken `<name>.enc`; it decrypts its own result and compares, writes it into the original file, reads it back, then renames to `<name>.enc` with `FileOperationsService.renameKeepingEditors` (now public; pins and recents follow). `decryptPermanently` renames first, then writes the plaintext (crash-safe order). `decryptForViewing` returns the text; `DecryptedViewService` shows it in a read-only `noteroot-decrypted:` document (content provider, text dropped on `onDidCloseTextDocument`). Commands in `encryptionCommands.ts` (`encryptNote` asks the key twice with `password: true`, `decryptView`, `decryptNote`; several files share a key). `NoteItem` adds the context suffix `.locked`, a `lock` icon and the click command `decryptView` for `.enc` files; menus group `7_encrypt`, and Run Tagging / Insert Header are hidden for `.locked`. Guards: `SearchService.readText` returns undefined for `.enc` (so search, tag scan, tagging and headers skip them), `NoteTaggingService` skips them, `QuickNoteService.validateTargetPath` / `writeText` refuse them. A link (symbolic) is refused, because a write would go through it to a file that may be outside the root. `decryptPermanently` gives the file its `.enc` name back if writing the plain text fails. `openFile` (every way of opening a file from the tree, search, Recent and Pinned) sends `.enc` files to `noteRoot.decryptView` instead of the editor. `tools/decrypt.js` (not shipped in the `.vsix`) decrypts a note with Node alone and `tools/decrypt.ps1` does the same in Windows PowerShell 5.1 or 7 with nothing installed (scrypt and AES-GCM are written in C# inside the script, because .NET Framework has neither); both must stay in step with `NoteCrypto.ts` (same layout, scrypt parameters and AAD). The key is never stored or logged. **Sending a note as text:** `noteRoot.copyEncrypted` (tree context menu of non-locked files, group `7_encrypt@3`) calls `NoteEncryptionService.encryptedTextOf` (reads the open editor text if the note is open, else the file; same refusals and round-trip check as `encrypt`; writes nothing) and puts the `NOTEROOT-ENCRYPTED v1` text on the clipboard; `noteRoot.decryptClipboard` (palette and the `...` menu of the Notes view, group `3_encrypt@1`) reads the clipboard, checks `looksEncrypted` on the trimmed text, asks the key and shows `decryptText` in the read-only `noteroot-decrypted:` document (`Strings.encryption.clipboardName`).
- **Show Hidden Items** (two commands that swap, like the Advanced mode: `noteRoot.showHiddenItems` "Show Hidden Items" while the context key `noteRoot.showHidden` is false and `noteRoot.hideHiddenItems` "Hide Hidden Items" while it is true, in the `...` menu of the Notes view, group `1_view@1`, and in the Command Palette; a contributed command cannot change its own title, and the `toggled` check mark was dropped): `ShowHiddenService` keeps the on/off state in `globalState` (`noteRoot.showHidden`, per machine), publishes the context key of the same name and fires `onDidChange`, which makes `NotesTreeProvider` refresh (it takes the service as its **last** constructor argument). In `getChildren` an entry that `ExcludeMatcher` hides is left out, unless the switch is on and the entry is not the config folder `.noteroot` (always out of sight); a shown one becomes a `NoteItem` with `hidden = true` (description "hidden"), and so does everything below a hidden folder (`element.hidden`). Only the tree changes: `SearchService`, `TagIndexService`, `AutoCategoryService`, Recent and Pinned keep honouring the exclude patterns.
- **Show in NoteRoot Again** (`noteRoot.includeItem`, tree context menu of **hidden** items only, `fileCommands.ts`): `ExcludeMatcher.matchingPatterns(name, relativePath)` gives the patterns of the list that hide an entry (`Rule` keeps the raw `pattern` and `builtin`, the config folder rule, which is never listed). `NoteItem` adds the suffix `.hidden` to its context value when `hidden`, so the menus test the start (`^noteRoot.(file|folder)`) and Exclude is shown with a negative-lookahead regex (there is no `!~` in `when` clauses), Include with `=~ /\.hidden$/`. For each hidden item: a pattern that is exactly `/<its path>` (any case) is taken out of the effective list at once; a pattern that matches more (`.git`, `*.tmp`) needs a modal confirmation (`Remove Pattern`); an item with no pattern of its own is hidden by a folder above it, and a message offers to include that folder (`Include the Folder`, only when that folder has an exact `/path` pattern). The list is written with `settings.setValue(excludePatterns, ...)` (folder list) and the message has Undo.
- **Don't Auto-Tag / Auto-Tag Again** (`noteRoot.excludeFromTagging` / `noteRoot.includeInTagging`, tree context menu of files and folders, `taggingCommands.ts`; palette hidden): the folder-only setting `noTagPatterns` (`custom.folderSettings.noTagPatterns`, list of patterns of the same syntax as the hidden list, default empty; row **Files and folders without tagging** in group Tags & Categories) read by a second `ExcludeMatcher` (`new ExcludeMatcher(settings, { key, read, always: [] })`: the constructor takes the setting key, a reader and the names that always match, `.noteroot` only for the hidden list), `noTagMatcher` in `extension.ts`. `NoteTaggingService` takes it as its **last** constructor argument: `tagNote` returns `{ status: "excluded" }` (before reading the file) and `planOnSave` returns undefined, for a note or a folder above it that the list matches (`isExcludedFromTagging`, every segment, like `isHidden`). Quick Note creation and the tags the user chooses are not affected (only automatic tagging). The tree: `NoteItem` has `noTag` (the matcher says so, or the folder it is in is `element.noTag`), description "no tags" ("hidden · no tags" with both) **only while the Show Hidden switch is on** (`markNoTag`, passed by the provider from `ShowHiddenService`), context value suffix `.notag` after `.hidden` **always** (so the menu knows Include from Exclude); the menus test them anywhere in the value; the "not" side (Exclude) is a positive regex with a negative lookahead, `/^noteRoot\.(file|folder)(?!.*\.notag)/`, because **when clauses have no `!~` operator** (with it the Exclude entries were always shown); the same for `.hidden`. The adding and taking out are the generic `addToPatternList` / `removeFromPatternList` of `patternLists.ts` (`PatternListOptions`: setting key, reader, matcher and the texts), which the hidden list uses too (Exclude/Include in `fileCommands.ts`).
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Open the **Actions** panel and choose **Connect to a Sync Service**. VS Code sig

Right-click a note and choose **Encrypt…**, then type a master key twice. The note is renamed `name.ext.enc` and shows a lock. Click it (or **Decrypt for Viewing…**) to read it in a read-only editor that exists only in memory; **Decrypt Permanently…** restores the note.

To send a note to someone, right-click it and choose **Copy as Encrypted Text…**: the encrypted text goes to the clipboard and the note stays as it is. The other person copies it and runs **NoteRoot: Decrypt Text from Clipboard…** (Command Palette, or the `...` menu of the Notes view; they need NoteRoot and the key).

- AES-256-GCM, with a key from your master key by scrypt. Tampering is detected. The file name and size stay visible.
- **The key is never stored and cannot be recovered. If you forget it, the note is lost.** Old copies (Local History, Git, backups) may still hold the plain text.
- Without the extension, `tools/decrypt.js` (Node.js) or `tools/decrypt.ps1` (PowerShell on Windows) decrypt a note. No independent security review yet.
Expand Down
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,14 @@
"command": "noteRoot.decryptNote",
"title": "Decrypt Permanently…"
},
{
"command": "noteRoot.copyEncrypted",
"title": "Copy as Encrypted Text…"
},
{
"command": "noteRoot.decryptClipboard",
"title": "NoteRoot: Decrypt Text from Clipboard…"
},
{
"command": "noteRoot.tagNote",
"title": "Run Tagging"
Expand Down Expand Up @@ -631,6 +639,11 @@
"when": "view == noteRoot.tree && noteRoot.state == ready",
"group": "1_edit@1"
},
{
"command": "noteRoot.decryptClipboard",
"when": "view == noteRoot.tree",
"group": "3_encrypt@1"
},
{
"command": "noteRoot.quickNoteOptions",
"when": "view == noteRoot.quickNote",
Expand Down Expand Up @@ -740,6 +753,10 @@
"command": "noteRoot.decryptNote",
"when": "false"
},
{
"command": "noteRoot.copyEncrypted",
"when": "false"
},
{
"command": "noteRoot.recreateRootFolder",
"when": "false"
Expand Down Expand Up @@ -1246,6 +1263,11 @@
"when": "view == noteRoot.tree && viewItem =~ /^noteRoot\\.file(?!.*\\.(locked|quickNote))/",
"group": "7_encrypt@1"
},
{
"command": "noteRoot.copyEncrypted",
"when": "view == noteRoot.tree && viewItem =~ /^noteRoot\\.file(?!.*\\.locked)/",
"group": "7_encrypt@3"
},
{
"command": "noteRoot.decryptView",
"when": "view == noteRoot.tree && viewItem =~ /^noteRoot\\.file.*\\.locked/",
Expand Down
Loading
Loading