mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-06 04:17:04 +01:00
Compare commits
20 Commits
dev/mjolle
...
user/yeela
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d977ae4f8e | ||
|
|
e3eb69b6c5 | ||
|
|
c790d39af7 | ||
|
|
d87dde132d | ||
|
|
534c411fd8 | ||
|
|
afeeea671f | ||
|
|
f1e045751a | ||
|
|
8682d0f54d | ||
|
|
1d13779697 | ||
|
|
37bd24db36 | ||
|
|
557a07589d | ||
|
|
2603efc8a9 | ||
|
|
dd138fb94b | ||
|
|
7cd201d355 | ||
|
|
9aab0f3893 | ||
|
|
91b7a99e76 | ||
|
|
d38edf798d | ||
|
|
17668047bf | ||
|
|
7b0b284d40 | ||
|
|
9aca6d136f |
35
.github/actions/spell-check/expect.txt
vendored
35
.github/actions/spell-check/expect.txt
vendored
@@ -216,6 +216,7 @@ CImage
|
||||
cla
|
||||
CLASSDC
|
||||
CLASSNOTAVAILABLE
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
CLIENTEDGE
|
||||
@@ -224,7 +225,6 @@ clientside
|
||||
CLIPBOARDUPDATE
|
||||
CLIPCHILDREN
|
||||
CLIPSIBLINGS
|
||||
CLITo
|
||||
closesocket
|
||||
clp
|
||||
CLSCTX
|
||||
@@ -254,6 +254,7 @@ colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
Convs
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -325,7 +326,6 @@ CUSTOMFORMATPLACEHOLDER
|
||||
CVal
|
||||
cvd
|
||||
CVirtual
|
||||
CVS
|
||||
CWMO
|
||||
CXSCREEN
|
||||
CXSMICON
|
||||
@@ -393,6 +393,7 @@ devpal
|
||||
dfx
|
||||
DIALOGEX
|
||||
digicert
|
||||
diffs
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
@@ -530,9 +531,12 @@ eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FARPROC
|
||||
fdw
|
||||
fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
FInc
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -574,6 +578,7 @@ formatetc
|
||||
FORPARSING
|
||||
foundrylocal
|
||||
FRAMECHANGED
|
||||
FRestore
|
||||
frm
|
||||
FROMTOUCH
|
||||
fsanitize
|
||||
@@ -608,6 +613,7 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
gfx
|
||||
GHND
|
||||
gitmodules
|
||||
GMEM
|
||||
@@ -658,6 +664,7 @@ hdwwiz
|
||||
Helpline
|
||||
helptext
|
||||
HGFE
|
||||
hgdiobj
|
||||
hglobal
|
||||
hhk
|
||||
HHmmssfff
|
||||
@@ -705,7 +712,7 @@ hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
HRAWINPUT
|
||||
HREDRAW
|
||||
hredraw
|
||||
hres
|
||||
hresult
|
||||
hrgn
|
||||
@@ -883,7 +890,7 @@ LINKOVERLAY
|
||||
LINQTo
|
||||
listview
|
||||
LIVEDRAW
|
||||
LIVEZOOM
|
||||
livezoom
|
||||
LLKH
|
||||
llkhf
|
||||
LMEM
|
||||
@@ -912,6 +919,7 @@ LPBITMAPINFOHEADER
|
||||
LPCFHOOKPROC
|
||||
LPCITEMIDLIST
|
||||
LPCLSID
|
||||
lpch
|
||||
lpcmi
|
||||
LPCMINVOKECOMMANDINFO
|
||||
LPCREATESTRUCT
|
||||
@@ -936,6 +944,7 @@ lptpm
|
||||
LPTR
|
||||
LPTSTR
|
||||
lpv
|
||||
LPrivate
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
@@ -984,6 +993,7 @@ mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
meme
|
||||
mcp
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
@@ -1074,11 +1084,12 @@ muxxc
|
||||
muxxh
|
||||
MVPs
|
||||
mvvm
|
||||
myorg
|
||||
myrepo
|
||||
MVVMTK
|
||||
MWBEx
|
||||
MYICON
|
||||
NAMECHANGE
|
||||
Notavailable
|
||||
namespaceanddescendants
|
||||
nao
|
||||
NCACTIVATE
|
||||
@@ -1221,6 +1232,7 @@ opensource
|
||||
openxmlformats
|
||||
ollama
|
||||
onnx
|
||||
openurl
|
||||
OPTIMIZEFORINVOKE
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
@@ -1298,6 +1310,7 @@ phwnd
|
||||
pici
|
||||
pidl
|
||||
PIDLIST
|
||||
PII
|
||||
pinfo
|
||||
pinvoke
|
||||
pipename
|
||||
@@ -1346,6 +1359,7 @@ ppv
|
||||
ppwsz
|
||||
prc
|
||||
Prefixer
|
||||
Premul
|
||||
prependpath
|
||||
prepopulate
|
||||
prevhost
|
||||
@@ -1495,6 +1509,7 @@ riid
|
||||
RKey
|
||||
RNumber
|
||||
rop
|
||||
rollups
|
||||
ROUNDSMALL
|
||||
ROWSETEXT
|
||||
rpcrt
|
||||
@@ -1639,6 +1654,7 @@ SKIPOWNPROCESS
|
||||
sku
|
||||
SLGP
|
||||
sln
|
||||
slnf
|
||||
slnx
|
||||
SMALLICON
|
||||
smartphone
|
||||
@@ -1805,7 +1821,6 @@ tlbimp
|
||||
tlc
|
||||
tmain
|
||||
TNP
|
||||
toolgood
|
||||
Toolhelp
|
||||
toolwindow
|
||||
TOPDOWNDIB
|
||||
@@ -1867,7 +1882,6 @@ Uniquifies
|
||||
unitconverter
|
||||
unittests
|
||||
UNLEN
|
||||
Uninitializes
|
||||
UNORM
|
||||
unremapped
|
||||
Unsubscribes
|
||||
@@ -1901,7 +1915,7 @@ valuegenerator
|
||||
variantassignment
|
||||
VARTYPE
|
||||
vcamp
|
||||
VCENTER
|
||||
vcenter
|
||||
vcgtq
|
||||
VCINSTALLDIR
|
||||
Vcpkg
|
||||
@@ -1933,7 +1947,7 @@ vorrq
|
||||
VOS
|
||||
vpaddlq
|
||||
vqsubq
|
||||
VREDRAW
|
||||
vredraw
|
||||
vreinterpretq
|
||||
VSC
|
||||
VSCBD
|
||||
@@ -1955,7 +1969,7 @@ vswhere
|
||||
Vtbl
|
||||
WANTNUKEWARNING
|
||||
WANTPALM
|
||||
wasdk
|
||||
WASDK
|
||||
wbem
|
||||
WBounds
|
||||
Wca
|
||||
@@ -2101,6 +2115,7 @@ xstyler
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
XXL
|
||||
xxxxxx
|
||||
YAxis
|
||||
ycombinator
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
model: Claude Sonnet 4.5
|
||||
description: 'Generate an 80-character git commit title for the local diff.'
|
||||
---
|
||||
|
||||
|
||||
2
.github/prompts/create-pr-summary.prompt.md
vendored
2
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
model: Claude Sonnet 4.5
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff.'
|
||||
---
|
||||
|
||||
|
||||
71
.github/prompts/fix-issue.prompt.md
vendored
Normal file
71
.github/prompts/fix-issue.prompt.md
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: " Execute the fix for a GitHub issue using the previously generated implementation plan. Apply code & tests directly in the repo. Output only a PR description (and optional manual steps)."
|
||||
---
|
||||
|
||||
# DEPENDENCY
|
||||
Source review prompt (for generating the implementation plan if missing):
|
||||
- .github/prompts/review-issue.prompt.md
|
||||
|
||||
Required plan file (single source of truth):
|
||||
- Generated Files/issueReview/{{issue_number}}/implementation-plan.md
|
||||
|
||||
## Dependency Handling
|
||||
1) If `implementation-plan.md` exists → proceed.
|
||||
2) If missing → run the review prompt:
|
||||
- Invoke: `.github/prompts/review-issue.prompt.md`
|
||||
- Pass: `issue_number={{issue_number}}`
|
||||
- Then re-check for `implementation-plan.md`.
|
||||
3) If still missing → stop and generate:
|
||||
- `Generated Files/issueFix/{{issue_number}}/manual-steps.md` containing:
|
||||
“implementation-plan.md not found; please run .github/prompts/review-issue.prompt.md for #{{issue_number}}.”
|
||||
|
||||
# GOAL
|
||||
For **#{{issue_number}}**:
|
||||
- Use implementation-plan.md as the single authority.
|
||||
- Apply code and test changes directly in the repository.
|
||||
- Produce a PR-ready description.
|
||||
|
||||
# OUTPUT FILES
|
||||
1) Generated Files/issueFix/{{issue_number}}/pr-description.md
|
||||
2) Generated Files/issueFix/{{issue_number}}/manual-steps.md # only if human interaction or external setup is required
|
||||
|
||||
# EXECUTION RULES
|
||||
1) Read implementation-plan.md and execute:
|
||||
- Layers & Files → edit/create as listed
|
||||
- Pattern Choices → follow repository conventions
|
||||
- Fundamentals (perf, security, compatibility, accessibility)
|
||||
- Logging & Exceptions
|
||||
- Telemetry (only if explicitly included in the plan)
|
||||
- Risks & Mitigations
|
||||
- Tests to Add
|
||||
2) Locate affected files via `rg` or `git grep`.
|
||||
3) Add/update tests to enforce the fixed behavior.
|
||||
4) If any ambiguity exists, add:
|
||||
// TODO(Human input needed): <clarification needed>
|
||||
5) Verify locally: build & tests run successfully.
|
||||
|
||||
# pr-description.md should include:
|
||||
- Title: `Fix: <short summary> (#{{issue_number}})`
|
||||
- What changed and why the fix works
|
||||
- Files or modules touched
|
||||
- Risks & mitigations (implemented)
|
||||
- Tests added/updated and how to run them
|
||||
- Telemetry behavior (if applicable)
|
||||
- Validation / reproduction steps
|
||||
- `Closes #{{issue_number}}`
|
||||
|
||||
# manual-steps.md (only if needed)
|
||||
- List required human actions: secrets, config, approvals, missing info, or code comments requiring human decisions.
|
||||
|
||||
# IMPORTANT
|
||||
- Apply code and tests directly; do not produce patch files.
|
||||
- Follow implementation-plan.md as the source of truth.
|
||||
- Insert comments for human review where a decision or input is required.
|
||||
- Use repository conventions and deterministic, minimal changes.
|
||||
|
||||
# FINALIZE
|
||||
- Write pr-description.md
|
||||
- Write manual-steps.md only if needed
|
||||
- Print concise success message or note items requiring human interaction
|
||||
158
.github/prompts/review-issue.prompt.md
vendored
Normal file
158
.github/prompts/review-issue.prompt.md
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: "You are github issue review and planning expertise, Score (0–100) and write one Implementation Plan. Outputs: overview.md, implementation-plan.md."
|
||||
---
|
||||
|
||||
# GOAL
|
||||
For **#{{issue_number}}** produce:
|
||||
1) `Generated Files/issueReview/{{issue_number}}/overview.md`
|
||||
2) `Generated Files/issueReview/{{issue_number}}/implementation-plan.md`
|
||||
|
||||
## Inputs
|
||||
figure out from the prompt on the
|
||||
|
||||
# CONTEXT (brief)
|
||||
Ground evidence using `gh issue view {{issue_number}} --json number,title,body,author,createdAt,updatedAt,state,labels,milestone,reactions,comments,linkedPullRequests`, and download the image for understand the context of the issue more.
|
||||
Locate source code in current workspace, but also free feel to use via `rg`/`git grep`. Link related issues/PRs.
|
||||
|
||||
# OVERVIEW.MD
|
||||
## Summary
|
||||
Issue, state, milestone, labels. **Signals**: 👍/❤️/👎, comment count, last activity, linked PRs.
|
||||
|
||||
## At-a-Glance Score Table
|
||||
Present all ratings in a compact table for quick scanning:
|
||||
|
||||
| Dimension | Score | Assessment | Key Drivers |
|
||||
|-----------|-------|------------|-------------|
|
||||
| **A) Business Importance** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **B) Community Excitement** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **C) Technical Feasibility** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **D) Requirement Clarity** | X/100 | Low/Medium/High | Top 2 factors with scores |
|
||||
| **Overall Priority** | X/100 | Low/Medium/High/Critical | Average or weighted summary |
|
||||
| **Effort Estimate** | X days (T-shirt) | XS/S/M/L/XL/XXL/Epic | Type: bug/feature/chore |
|
||||
| **Similar Issues Found** | X open, Y closed | — | Quick reference to related work |
|
||||
| **Potential Assignees** | @username, @username | — | Top contributors to module |
|
||||
|
||||
**Assessment bands**: 0-25 Low, 26-50 Medium, 51-75 High, 76-100 Critical
|
||||
|
||||
## Ratings (0–100) — add evidence & short rationale
|
||||
### A) Business Importance
|
||||
- Labels (priority/security/regression): **≤35**
|
||||
- Milestone/roadmap: **≤25**
|
||||
- Customer/contract impact: **≤20**
|
||||
- Unblocks/platform leverage: **≤20**
|
||||
### B) Community Excitement
|
||||
- 👍+❤️ normalized: **≤45**
|
||||
- Comment volume & unique participants: **≤25**
|
||||
- Recent activity (≤30d): **≤15**
|
||||
- Duplicates/related issues: **≤15**
|
||||
### C) Technical Feasibility
|
||||
- Contained surface/clear seams: **≤30**
|
||||
- Existing patterns/utilities: **≤25**
|
||||
- Risk (perf/sec/compat) manageable: **≤25**
|
||||
- Testability & CI support: **≤20**
|
||||
### D) Requirement Clarity
|
||||
- Behavior/repro/constraints: **≤60**
|
||||
- Non-functionals (perf/sec/i18n/a11y): **≤25**
|
||||
- Decision owners/acceptance signals: **≤15**
|
||||
|
||||
## Effort
|
||||
Days + **T-shirt** (XS 0.5–1d, S 1–2, M 2–4, L 4–7, XL 7–14, XXL 14–30, Epic >30).
|
||||
Type/level: bug/feature/chore/docs/refactor/test-only; severity/value tier.
|
||||
|
||||
## Suggested Actions
|
||||
Provide actionable recommendations for issue triage and assignment:
|
||||
|
||||
### A) Requirement Clarification (if Clarity score <50)
|
||||
**When Requirement Clarity (Dimension D) is Medium or Low:**
|
||||
- Identify specific gaps in issue description: missing repro steps, unclear expected behavior, undefined acceptance criteria, missing non-functional requirements
|
||||
- Draft 3-5 clarifying questions to post as issue comment
|
||||
- Suggest additional information needed: screenshots, logs, environment details, OS version, PowerToys version, error messages
|
||||
- If behavior is ambiguous, propose 2-3 interpretation scenarios and ask reporter to confirm
|
||||
- Example questions:
|
||||
- "Can you provide exact steps to reproduce this issue?"
|
||||
- "What is the expected behavior vs. what you're actually seeing?"
|
||||
- "Does this happen on Windows 10, 11, or both?"
|
||||
- "Can you attach a screenshot or screen recording?"
|
||||
|
||||
### B) Correct Label Suggestions
|
||||
- Analyze issue type, module, and severity to suggest missing or incorrect labels
|
||||
- Recommend labels from: `Issue-Bug`, `Issue-Feature`, `Issue-Docs`, `Issue-Task`, `Priority-High`, `Priority-Medium`, `Priority-Low`, `Needs-Triage`, `Needs-Author-Feedback`, `Product-<ModuleName>`, etc.
|
||||
- If Requirement Clarity is low (<50), add `Needs-Author-Feedback` label
|
||||
- If current labels are incorrect or incomplete, provide specific label changes with rationale
|
||||
|
||||
### C) Find Similar Issues & Past Fixes
|
||||
- Search for similar issues using `gh issue list --search "keywords" --state all --json number,title,state,closedAt`
|
||||
- Identify patterns: duplicate issues, related bugs, or similar feature requests
|
||||
- For closed issues, find linked PRs that fixed them: check `linkedPullRequests` in issue data
|
||||
- Provide 3-5 examples of similar issues with format: `#<number> - <title> (closed by PR #<pr>)` or `(still open)`
|
||||
|
||||
### D) Identify Subject Matter Experts
|
||||
- Use git blame/log to find who fixed similar issues in the past
|
||||
- Search for PR authors who touched relevant files: `git log --all --format='%aN' -- <file_paths> | sort | uniq -c | sort -rn | head -5`
|
||||
- Check issue/PR history for frequent contributors to the affected module
|
||||
- Suggest 2-3 potential assignees with context: `@<username> - <reason>` (e.g., "fixed similar rendering bug in #12345", "maintains FancyZones module")
|
||||
|
||||
### E) Semantic Search for Related Work
|
||||
- Use semantic_search tool to find similar issues, code patterns, or past discussions
|
||||
- Search queries should include: issue keywords, module names, error messages, feature descriptions
|
||||
- Cross-reference semantic results with GitHub issue search for comprehensive coverage
|
||||
|
||||
**Output format for Suggested Actions section in overview.md:**
|
||||
```markdown
|
||||
## Suggested Actions
|
||||
|
||||
### Clarifying Questions (if Clarity <50)
|
||||
Post these questions as issue comment to gather missing information:
|
||||
1. <question>
|
||||
2. <question>
|
||||
3. <question>
|
||||
|
||||
**Recommended label**: `Needs-Author-Feedback`
|
||||
|
||||
### Label Recommendations
|
||||
- Add: `<label>` - <reason>
|
||||
- Remove: `<label>` - <reason>
|
||||
- Current labels are appropriate ✓
|
||||
|
||||
### Similar Issues Found
|
||||
1. #<number> - <title> (<state>, closed by PR #<pr> on <date>)
|
||||
2. #<number> - <title> (<state>)
|
||||
...
|
||||
|
||||
### Potential Assignees
|
||||
- @<username> - <reason>
|
||||
- @<username> - <reason>
|
||||
|
||||
### Related Code/Discussions
|
||||
- <semantic search findings>
|
||||
```
|
||||
|
||||
# IMPLEMENTATION-PLAN.MD
|
||||
1) **Problem Framing** — restate problem; current vs expected; scope boundaries.
|
||||
2) **Layers & Files** — layers (UI/domain/data/infra/build). For each, list **files/dirs to modify** and **new files** (exact paths + why). Prefer repo patterns; cite examples/PRs.
|
||||
3) **Pattern Choices** — reuse existing; if new, justify trade-offs & transition.
|
||||
4) **Fundamentals** (brief plan or N/A + reason):
|
||||
- Performance (hot paths, allocs, caching/streaming)
|
||||
- Security (validation, authN/Z, secrets, SSRF/XSS/CSRF)
|
||||
- G11N/L10N (resources, number/date, pluralization)
|
||||
- Compatibility (public APIs, formats, OS/runtime/toolchain)
|
||||
- Extensibility (DI seams, options/flags, plugin points)
|
||||
- Accessibility (roles, labels, focus, keyboard, contrast)
|
||||
- SOLID & repo conventions (naming, folders, dependency direction)
|
||||
5) **Logging & Exception Handling**
|
||||
- Where to log; levels; structured fields; correlation/traces.
|
||||
- What to catch vs rethrow; retries/backoff; user-visible errors.
|
||||
- **Privacy**: never log secrets/PII; redaction policy.
|
||||
6) **Telemetry (optional — business metrics only)**
|
||||
- Events/metrics (name, when, props); success signal; privacy/sampling; dashboards/alerts.
|
||||
7) **Risks & Mitigations** — flags/canary/shadow-write/config guards.
|
||||
8) **Task Breakdown (agent-ready)** — table (leave a blank line before the header so Markdown renders correctly):
|
||||
|
||||
| Task | Intent | Files/Areas | Steps | Tests (brief) | Owner (Agent/Human) | Human interaction needed? (why) |
|
||||
|---|---|---|---|---|---|---|
|
||||
|
||||
9) **Tests to Add (only)**
|
||||
- **Unit**: targets, cases (success/edge/error), mocks/fixtures, path, notes.
|
||||
- **UI** (if applicable): flows, locator strategy, env/data/flags, path, flake mitigation.
|
||||
199
.github/prompts/review-pr.prompt.md
vendored
Normal file
199
.github/prompts/review-pr.prompt.md
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: "gh-driven PR review; per-step Markdown + machine-readable outputs"
|
||||
---
|
||||
|
||||
# PR Review — gh + stepwise
|
||||
|
||||
**Goal**: Given `{{pr_number}}`, run a *one-topic-per-step* review. Write files to `Generated Files/prReview/{{pr_number}}/` (replace `{{pr_number}}` with the integer). Emit machine‑readable blocks for a GitHub MCP to post review comments.
|
||||
|
||||
## PR selection
|
||||
Resolve the target PR using these fallbacks in order:
|
||||
1. Parse the invocation text for an explicit identifier (first integer following patterns such as a leading hash and digits or the text `PR:` followed by digits).
|
||||
2. If no PR is found yet, locate the newest `Generated Files/prReview/_batch/batch-overview-*.md` file (highest timestamp in filename, fallback newest mtime) and take the first entry in its `## PRs` list whose review folder is missing `00-OVERVIEW.md` or contains `__error.flag`.
|
||||
3. If the batch file has no pending PRs, query assignments with `gh pr list --assignee @me --state open --json number,updatedAt --limit 20` and pick the most recently updated PR that does not already have a completed review folder.
|
||||
4. If still unknown, run `gh pr view --json number` in the current branch and use that result when it is unambiguous.
|
||||
5. If every step above fails, prompt the user for a PR number before proceeding.
|
||||
|
||||
## Fetch PR data with `gh`
|
||||
- `gh pr view {{pr_number}} --json number,baseRefName,headRefName,baseRefOid,headRefOid,changedFiles,files`
|
||||
- `gh api repos/:owner/:repo/pulls/{{pr_number}}/files?per_page=250` # patches for line mapping
|
||||
|
||||
### Incremental review workflow
|
||||
1. **Check for existing review**: Read `Generated Files/prReview/{{pr_number}}/00-OVERVIEW.md`
|
||||
2. **Extract state**: Parse `Last reviewed SHA:` from review metadata section
|
||||
3. **Detect changes**: Run `Get-PrIncrementalChanges.ps1 -PullRequestNumber {{pr_number}} -LastReviewedCommitSha {{sha}}`
|
||||
4. **Analyze result**:
|
||||
- `NeedFullReview: true` → Review all files in the PR
|
||||
- `NeedFullReview: false` and `IsIncremental: true` → Review only files in `ChangedFiles` array
|
||||
- `ChangedFiles` is empty → No changes, skip review (update iteration history with "No changes since last review")
|
||||
5. **Apply smart filtering**: Use the file patterns in smart step filtering table to skip irrelevant steps
|
||||
6. **Update metadata**: After completing review, save current `headRefOid` as `Last reviewed SHA:` in `00-OVERVIEW.md`
|
||||
|
||||
### Reusable PowerShell scripts
|
||||
Scripts live in `.github/review-tools/` to avoid repeated manual approvals during PR reviews:
|
||||
|
||||
| Script | Usage |
|
||||
| --- | --- |
|
||||
| `.github/review-tools/Get-GitHubRawFile.ps1` | Download a repository file at a given ref, optionally with line numbers. |
|
||||
| `.github/review-tools/Get-GitHubPrFilePatch.ps1` | Fetch the unified diff for a specific file within a pull request via `gh api`. |
|
||||
| `.github/review-tools/Get-PrIncrementalChanges.ps1` | Compare last reviewed SHA with current PR head to identify incremental changes. Returns JSON with changed files, new commits, and whether full review is needed. |
|
||||
| `.github/review-tools/Test-IncrementalReview.ps1` | Test helper to preview incremental review detection for a PR. Use before running full review to see what changed. |
|
||||
|
||||
Always prefer these scripts (or new ones added under `.github/review-tools/`) over raw `gh api` or similar shell commands so the review flow does not trigger interactive approval prompts.
|
||||
|
||||
## Output files
|
||||
Folder: `Generated Files/prReview/{{pr_number}}/`
|
||||
Files: `00-OVERVIEW.md`, `01-functionality.md`, `02-compatibility.md`, `03-performance.md`, `04-accessibility.md`, `05-security.md`, `06-localization.md`, `07-globalization.md`, `08-extensibility.md`, `09-solid-design.md`, `10-repo-patterns.md`, `11-docs-automation.md`, `12-code-comments.md`, `13-copilot-guidance.md` *(only if guidance md exists).*
|
||||
- **Write-after-step rule:** Immediately after completing each TODO step, persist that step's markdown file before proceeding to the next. Generate `00-OVERVIEW.md` only after every step file has been refreshed for the current run.
|
||||
|
||||
## Iteration management
|
||||
- Determine the current review iteration by reading `00-OVERVIEW.md` (look for `Review iteration:`). If missing, assume iteration `1`.
|
||||
- Extract the last reviewed SHA from `00-OVERVIEW.md` (look for `Last reviewed SHA:` in the review metadata section). If missing, this is iteration 1.
|
||||
- **Incremental review detection**:
|
||||
1. Call `.github/review-tools/Get-PrIncrementalChanges.ps1 -PullRequestNumber {{pr_number}} -LastReviewedCommitSha {{last_sha}}` to get delta analysis.
|
||||
2. Parse the JSON result to determine if incremental review is possible (`IsIncremental: true`, `NeedFullReview: false`).
|
||||
3. If force-push detected or first review, proceed with full review of all changed files.
|
||||
4. If incremental, review only the files listed in `ChangedFiles` array and apply smart step filtering (see below).
|
||||
- Increment the iteration for each review run and propagate the new value to all step files and the overview.
|
||||
- Preserve prior iteration notes by keeping/expanding an `## Iteration history` section in each markdown file, appending the newest summary under `### Iteration <N>`.
|
||||
- Summaries should capture key deltas since the previous iteration so reruns can pick up context quickly.
|
||||
- **After review completion**, update `Last reviewed SHA:` in `00-OVERVIEW.md` with the current `headRefOid` and update the timestamp.
|
||||
|
||||
### Smart step filtering (incremental reviews only)
|
||||
When performing incremental review, skip steps that are irrelevant based on changed file types:
|
||||
|
||||
| File pattern | Required steps | Skippable steps |
|
||||
| --- | --- | --- |
|
||||
| `**/*.cs`, `**/*.cpp`, `**/*.h` | Functionality, Compatibility, Performance, Security, SOLID, Repo patterns, Code comments | (depends on files) |
|
||||
| `**/*.resx`, `**/Resources/*.xaml` | Localization, Globalization | Most others |
|
||||
| `**/*.md` (docs) | Docs & automation | Most others (unless copilot guidance) |
|
||||
| `**/*copilot*.md`, `.github/prompts/*.md` | Copilot guidance, Docs & automation | Most others |
|
||||
| `**/*.csproj`, `**/*.vcxproj`, `**/packages.config` | Compatibility, Security, Repo patterns | Localization, Globalization, Accessibility |
|
||||
| `**/UI/**`, `**/*View.xaml` | Accessibility, Localization | Performance (unless perf-sensitive controls) |
|
||||
|
||||
**Default**: If uncertain or files span multiple categories, run all applicable steps. When in doubt, be conservative and review more rather than less.
|
||||
|
||||
## TODO steps (one concern each)
|
||||
1) Functionality
|
||||
2) Compatibility
|
||||
3) Performance
|
||||
4) Accessibility
|
||||
5) Security
|
||||
6) Localization
|
||||
7) Globalization
|
||||
8) Extensibility
|
||||
9) SOLID principles
|
||||
10) Repo patterns
|
||||
11) Docs & automation coverage for the changes
|
||||
12) Code comments
|
||||
13) Copilot guidance (conditional): if changed folders contain `*copilot*.md` or `.github/prompts/*.md`, review diffs **against** that guidance and write `13-copilot-guidance.md` (omit if none).
|
||||
|
||||
## Per-step file template (use verbatim)
|
||||
```md
|
||||
# <STEP TITLE>
|
||||
**PR:** (populate with PR identifier) — Base:<baseRefName> Head:<headRefName>
|
||||
**Review iteration:** ITERATION
|
||||
|
||||
## Iteration history
|
||||
- Maintain subsections titled `### Iteration N` in reverse chronological order (append the latest at the top) with 2–4 bullet highlights.
|
||||
|
||||
### Iteration ITERATION
|
||||
- <Latest key point 1>
|
||||
- <Latest key point 2>
|
||||
|
||||
## Checks executed
|
||||
- List the concrete checks for *this step only* (5–10 bullets).
|
||||
|
||||
## Findings
|
||||
(If none, write **None**. Defaults have one or more blocks:)
|
||||
|
||||
```mcp-review-comment
|
||||
{"file":"relative/path.ext","start_line":123,"end_line":125,"severity":"high|medium|low|info","tags":["<step-slug>","pr-tag-here"],"related_files":["optional/other/file1"],"body":"Problem → Why it matters → Concrete fix. If spans multiple files, name them here."}
|
||||
```
|
||||
Use the second tag to encode the PR number.
|
||||
|
||||
```
|
||||
## Overview file (`00-OVERVIEW.md`) template
|
||||
```md
|
||||
# PR Review Overview — (populate with PR identifier)
|
||||
**Review iteration:** ITERATION
|
||||
**Changed files:** <n> | **High severity issues:** <count>
|
||||
|
||||
## Review metadata
|
||||
**Last reviewed SHA:** <headRefOid from gh pr view>
|
||||
**Last review timestamp:** <ISO8601 timestamp>
|
||||
**Review mode:** <Full|Incremental (N files changed since iteration X)>
|
||||
**Base ref:** <baseRefName>
|
||||
**Head ref:** <headRefName>
|
||||
|
||||
## Step results
|
||||
Write lines like: `01 Functionality — <OK|Issues|Skipped> (see 01-functionality.md)` … through step 13.
|
||||
Mark steps as "Skipped" when using incremental review smart filtering.
|
||||
|
||||
## Iteration history
|
||||
- Maintain subsections titled `### Iteration N` mirroring the per-step convention with concise deltas and cross-links to the relevant step files.
|
||||
- For incremental reviews, list the specific files that changed and which commits were added.
|
||||
```
|
||||
|
||||
## Line numbers & multi‑file issues
|
||||
- Map head‑side lines from `patch` hunks (`@@ -a,b +c,d @@` → new lines `+c..+c+d-1`).
|
||||
- For cross‑file issues: set the primary `"file"`, list others in `"related_files"`, and name them in `"body"`.
|
||||
|
||||
## Posting (for MCP)
|
||||
- Parse all ```mcp-review-comment``` blocks across step files and post as PR review comments.
|
||||
- If posting isn’t available, still write all files.
|
||||
|
||||
## Constraint
|
||||
Read/analyze only; don't modify code. Keep comments small, specific, and fix‑oriented.
|
||||
|
||||
**Testing**: Use `.github/review-tools/Test-IncrementalReview.ps1 -PullRequestNumber 42374` to preview incremental detection before running full review.
|
||||
|
||||
## Scratch cache for large PRs
|
||||
|
||||
Create a local scratch workspace to progressively summarize diffs and reload state across runs.
|
||||
|
||||
### Paths
|
||||
- Root: `Generated Files/prReview/{{pr_number}}/__tmp/`
|
||||
- Files:
|
||||
- `index.jsonl` — append-only JSON Lines index of artifacts.
|
||||
- `todo-queue.json` — pending items (files/chunks/steps).
|
||||
- `rollup-<step>-v<N>.md` — iterative per-step aggregates.
|
||||
- `file-<hash>.txt` — optional saved chunk text (when needed).
|
||||
|
||||
### JSON schema (per line in `index.jsonl`)
|
||||
```json
|
||||
{"type":"chunk|summary|issue|crosslink",
|
||||
"path":"relative/file.ext","chunk_id":"f-12","step":"functionality|compatibility|...",
|
||||
"base_sha":"...", "head_sha":"...", "range":[start,end], "version":1,
|
||||
"notes":"short text or key:value map", "created_utc":"ISO8601"}
|
||||
```
|
||||
|
||||
### Phases (stateful; resume-safe)
|
||||
0. **Discover** PR + SHAs: `gh pr view <PR> --json baseRefName,headRefName,baseRefOid,headRefOid,files`.
|
||||
1. **Chunk** each changed file (head): split into ~300–600 LOC or ~4k chars; stable `chunk_id` = hash(path+start).
|
||||
- Save `chunk` records. Optionally write `file-<hash>.txt` for expensive chunks.
|
||||
2. **Summarize** per chunk: intent, APIs, risks per TODO step; emit `summary` records (≤600 tokens each).
|
||||
3. **Issues**: convert findings to machine-readable blocks and emit `issue` records (later rendered to step MD).
|
||||
4. **Rollups**: build/update `rollup-<step>-v<N>.md` from `summary`+`issue`. Keep prior versions.
|
||||
5. **Finalize**: write per-step files + `00-OVERVIEW.md` from rollups. Post comments via MCP if available.
|
||||
|
||||
### Re-use & token limits
|
||||
- Always **reload** `index.jsonl` first; skip chunks with same `head_sha` and `range`.
|
||||
- **Incremental review optimization**: When `Get-PrIncrementalChanges.ps1` returns a subset of changed files, load only chunks from those files. Reuse existing chunks/summaries for unchanged files.
|
||||
- Prefer re-summarizing only changed chunks; merge chunk summaries → file summaries → step rollups.
|
||||
- When context is tight, load only the minimal chunk text (or its saved `file-<hash>.txt`) needed for a comment.
|
||||
|
||||
### Original vs diff
|
||||
- Fetch base content when needed: prefer `git show <baseRefName>:<path>`; fallback `gh api repos/:owner/:repo/contents/<path>?ref=<base_sha>` (base64).
|
||||
- Use patch hunks from `gh api .../pulls/<PR>/files` to compute **head** line numbers.
|
||||
|
||||
### Queue-driven loop
|
||||
- Seed `todo-queue.json` with all changed files.
|
||||
- Process: chunk → summarize → detect issues → roll up.
|
||||
- Append to `index.jsonl` after each step; never rewrite previous lines (append-only).
|
||||
|
||||
### Hygiene
|
||||
- `__tmp/` is implementation detail; do not include in final artifacts.
|
||||
- It is safe to delete to force a clean pass; the next run rebuilds it.
|
||||
79
.github/review-tools/Get-GitHubPrFilePatch.ps1
vendored
Normal file
79
.github/review-tools/Get-GitHubPrFilePatch.ps1
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Retrieves the unified diff patch for a specific file in a GitHub pull request.
|
||||
|
||||
.DESCRIPTION
|
||||
This script fetches the patch content (unified diff format) for a specified file
|
||||
within a pull request. It uses the GitHub CLI (gh) to query the GitHub API and
|
||||
retrieve file change information.
|
||||
|
||||
.PARAMETER PullRequestNumber
|
||||
The pull request number to query.
|
||||
|
||||
.PARAMETER FilePath
|
||||
The relative path to the file in the repository (e.g., "src/modules/main.cpp").
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath "src/modules/cmdpal/main.cpp"
|
||||
Retrieves the patch for main.cpp in PR #42374.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath "README.md" -RepositoryOwner "myorg" -RepositoryName "myrepo"
|
||||
Retrieves the patch from a different repository.
|
||||
|
||||
.NOTES
|
||||
Requires GitHub CLI (gh) to be installed and authenticated.
|
||||
Run 'gh auth login' if not already authenticated.
|
||||
|
||||
.LINK
|
||||
https://cli.github.com/
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Pull request number")]
|
||||
[int]$PullRequestNumber,
|
||||
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Relative path to the file in the repository")]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys"
|
||||
)
|
||||
|
||||
# Construct GitHub API path for pull request files
|
||||
$apiPath = "repos/$RepositoryOwner/$RepositoryName/pulls/$PullRequestNumber/files?per_page=250"
|
||||
|
||||
# Query GitHub API to get all files in the pull request
|
||||
try {
|
||||
$pullRequestFiles = gh api $apiPath | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Error "Failed to query GitHub API for PR #$PullRequestNumber. Ensure gh CLI is authenticated. Details: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Find the matching file in the pull request
|
||||
$matchedFile = $pullRequestFiles | Where-Object { $_.filename -eq $FilePath }
|
||||
|
||||
if (-not $matchedFile) {
|
||||
Write-Error "File '$FilePath' not found in PR #$PullRequestNumber."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if patch content exists
|
||||
if (-not $matchedFile.patch) {
|
||||
Write-Warning "File '$FilePath' has no patch content (possibly binary or too large)."
|
||||
return
|
||||
}
|
||||
|
||||
# Output the patch content
|
||||
$matchedFile.patch
|
||||
91
.github/review-tools/Get-GitHubRawFile.ps1
vendored
Normal file
91
.github/review-tools/Get-GitHubRawFile.ps1
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Downloads and displays the content of a file from a GitHub repository at a specific git reference.
|
||||
|
||||
.DESCRIPTION
|
||||
This script fetches the raw content of a file from a GitHub repository using GitHub's raw content API.
|
||||
It can optionally display line numbers and supports any valid git reference (branch, tag, or commit SHA).
|
||||
|
||||
.PARAMETER FilePath
|
||||
The relative path to the file in the repository (e.g., "src/modules/main.cpp").
|
||||
|
||||
.PARAMETER GitReference
|
||||
The git reference (branch name, tag, or commit SHA) to fetch the file from. Defaults to "main".
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.PARAMETER ShowLineNumbers
|
||||
When specified, displays line numbers before each line of content.
|
||||
|
||||
.PARAMETER StartLineNumber
|
||||
The starting line number to use when ShowLineNumbers is enabled. Defaults to 1.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "README.md" -GitReference "main"
|
||||
Downloads and displays the README.md file from the main branch.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "src/runner/main.cpp" -GitReference "dev/feature-branch" -ShowLineNumbers
|
||||
Downloads main.cpp from a feature branch and displays it with line numbers.
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "LICENSE" -GitReference "abc123def" -ShowLineNumbers -StartLineNumber 10
|
||||
Downloads the LICENSE file from a specific commit and displays it with line numbers starting at 10.
|
||||
|
||||
.NOTES
|
||||
Requires internet connectivity to access GitHub's raw content API.
|
||||
Does not require GitHub CLI authentication for public repositories.
|
||||
|
||||
.LINK
|
||||
https://docs.github.com/en/rest/repos/contents
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Relative path to the file in the repository")]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Git reference (branch, tag, or commit SHA)")]
|
||||
[string]$GitReference = "main",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Display line numbers before each line")]
|
||||
[switch]$ShowLineNumbers,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Starting line number for display")]
|
||||
[int]$StartLineNumber = 1
|
||||
)
|
||||
|
||||
# Construct the raw content URL
|
||||
$rawContentUrl = "https://raw.githubusercontent.com/$RepositoryOwner/$RepositoryName/$GitReference/$FilePath"
|
||||
|
||||
# Fetch the file content from GitHub
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri $rawContentUrl
|
||||
} catch {
|
||||
Write-Error "Failed to fetch file from $rawContentUrl. Details: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Split content into individual lines
|
||||
$contentLines = $response.Content -split "`n"
|
||||
|
||||
# Display the content with or without line numbers
|
||||
if ($ShowLineNumbers) {
|
||||
$currentLineNumber = $StartLineNumber
|
||||
foreach ($line in $contentLines) {
|
||||
Write-Output ("{0:d4}: {1}" -f $currentLineNumber, $line)
|
||||
$currentLineNumber++
|
||||
}
|
||||
} else {
|
||||
$contentLines | ForEach-Object { Write-Output $_ }
|
||||
}
|
||||
173
.github/review-tools/Get-PrIncrementalChanges.ps1
vendored
Normal file
173
.github/review-tools/Get-PrIncrementalChanges.ps1
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Detects changes between the last reviewed commit and current head of a pull request.
|
||||
|
||||
.DESCRIPTION
|
||||
This script compares a previously reviewed commit SHA with the current head of a pull request
|
||||
to determine what has changed. It helps enable incremental reviews by identifying new commits
|
||||
and modified files since the last review iteration.
|
||||
|
||||
The script handles several scenarios:
|
||||
- First review (no previous SHA provided)
|
||||
- No changes (current SHA matches last reviewed SHA)
|
||||
- Force-push detected (last reviewed SHA no longer in history)
|
||||
- Incremental changes (new commits added since last review)
|
||||
|
||||
.PARAMETER PullRequestNumber
|
||||
The pull request number to analyze.
|
||||
|
||||
.PARAMETER LastReviewedCommitSha
|
||||
The commit SHA that was last reviewed. If omitted, this is treated as a first review.
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.OUTPUTS
|
||||
JSON object containing:
|
||||
- PullRequestNumber: The PR number being analyzed
|
||||
- CurrentHeadSha: The current head commit SHA
|
||||
- LastReviewedSha: The last reviewed commit SHA (if provided)
|
||||
- BaseRefName: Base branch name
|
||||
- HeadRefName: Head branch name
|
||||
- IsIncremental: Boolean indicating if incremental review is possible
|
||||
- NeedFullReview: Boolean indicating if a full review is required
|
||||
- ChangedFiles: Array of files that changed (filename, status, additions, deletions)
|
||||
- NewCommits: Array of commits added since last review (sha, message, author, date)
|
||||
- Summary: Human-readable description of changes
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374
|
||||
Analyzes PR #42374 with no previous review (first review scenario).
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123def456"
|
||||
Compares current PR state against the last reviewed commit to identify incremental changes.
|
||||
|
||||
.EXAMPLE
|
||||
$changes = .\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123" | ConvertFrom-Json
|
||||
if ($changes.IsIncremental) { Write-Host "Can perform incremental review" }
|
||||
Captures the output as a PowerShell object for further processing.
|
||||
|
||||
.NOTES
|
||||
Requires GitHub CLI (gh) to be installed and authenticated.
|
||||
Run 'gh auth login' if not already authenticated.
|
||||
|
||||
.LINK
|
||||
https://cli.github.com/
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Pull request number")]
|
||||
[int]$PullRequestNumber,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Commit SHA that was last reviewed")]
|
||||
[string]$LastReviewedCommitSha,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys"
|
||||
)
|
||||
|
||||
# Fetch current pull request state from GitHub
|
||||
try {
|
||||
$pullRequestData = gh pr view $PullRequestNumber --json headRefOid,headRefName,baseRefName,baseRefOid | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Error "Failed to fetch PR #$PullRequestNumber details. Details: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$currentHeadSha = $pullRequestData.headRefOid
|
||||
$baseRefName = $pullRequestData.baseRefName
|
||||
$headRefName = $pullRequestData.headRefName
|
||||
|
||||
# Initialize result object
|
||||
$analysisResult = @{
|
||||
PullRequestNumber = $PullRequestNumber
|
||||
CurrentHeadSha = $currentHeadSha
|
||||
BaseRefName = $baseRefName
|
||||
HeadRefName = $headRefName
|
||||
LastReviewedSha = $LastReviewedCommitSha
|
||||
IsIncremental = $false
|
||||
NeedFullReview = $true
|
||||
ChangedFiles = @()
|
||||
NewCommits = @()
|
||||
Summary = ""
|
||||
}
|
||||
|
||||
# Scenario 1: First review (no previous SHA provided)
|
||||
if ([string]::IsNullOrWhiteSpace($LastReviewedCommitSha)) {
|
||||
$analysisResult.Summary = "Initial review - no previous iteration found"
|
||||
$analysisResult.NeedFullReview = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
# Scenario 2: No changes since last review
|
||||
if ($currentHeadSha -eq $LastReviewedCommitSha) {
|
||||
$analysisResult.Summary = "No changes since last review (SHA: $currentHeadSha)"
|
||||
$analysisResult.NeedFullReview = $false
|
||||
$analysisResult.IsIncremental = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
# Scenario 3: Check for force-push (last reviewed SHA no longer exists in history)
|
||||
try {
|
||||
$null = gh api "repos/$RepositoryOwner/$RepositoryName/commits/$LastReviewedCommitSha" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
# SHA not found - likely force-push or branch rewrite
|
||||
$analysisResult.Summary = "Force-push detected - last reviewed SHA $LastReviewedCommitSha no longer exists. Full review required."
|
||||
$analysisResult.NeedFullReview = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
} catch {
|
||||
$analysisResult.Summary = "Cannot verify last reviewed SHA $LastReviewedCommitSha - assuming force-push. Full review required."
|
||||
$analysisResult.NeedFullReview = $true
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
}
|
||||
|
||||
# Scenario 4: Get incremental changes between last reviewed SHA and current head
|
||||
try {
|
||||
$compareApiPath = "repos/$RepositoryOwner/$RepositoryName/compare/$LastReviewedCommitSha...$currentHeadSha"
|
||||
$comparisonData = gh api $compareApiPath | ConvertFrom-Json
|
||||
|
||||
# Extract new commits information
|
||||
$analysisResult.NewCommits = $comparisonData.commits | ForEach-Object {
|
||||
@{
|
||||
Sha = $_.sha.Substring(0, 7)
|
||||
Message = $_.commit.message.Split("`n")[0] # First line only
|
||||
Author = $_.commit.author.name
|
||||
Date = $_.commit.author.date
|
||||
}
|
||||
}
|
||||
|
||||
# Extract changed files information
|
||||
$analysisResult.ChangedFiles = $comparisonData.files | ForEach-Object {
|
||||
@{
|
||||
Filename = $_.filename
|
||||
Status = $_.status # added, modified, removed, renamed
|
||||
Additions = $_.additions
|
||||
Deletions = $_.deletions
|
||||
Changes = $_.changes
|
||||
}
|
||||
}
|
||||
|
||||
$fileCount = $analysisResult.ChangedFiles.Count
|
||||
$commitCount = $analysisResult.NewCommits.Count
|
||||
|
||||
$analysisResult.IsIncremental = $true
|
||||
$analysisResult.NeedFullReview = $false
|
||||
$analysisResult.Summary = "Incremental review: $commitCount new commit(s), $fileCount file(s) changed since SHA $($LastReviewedCommitSha.Substring(0, 7))"
|
||||
|
||||
} catch {
|
||||
Write-Error "Failed to compare commits. Details: $_"
|
||||
$analysisResult.Summary = "Error comparing commits - defaulting to full review"
|
||||
$analysisResult.NeedFullReview = $true
|
||||
}
|
||||
|
||||
# Return the analysis result as JSON
|
||||
return $analysisResult | ConvertTo-Json -Depth 10
|
||||
170
.github/review-tools/Test-IncrementalReview.ps1
vendored
Normal file
170
.github/review-tools/Test-IncrementalReview.ps1
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests and previews incremental review detection for a pull request.
|
||||
|
||||
.DESCRIPTION
|
||||
This helper script validates the incremental review detection logic by analyzing an existing
|
||||
PR review folder. It reads the last reviewed SHA from the overview file, compares it with
|
||||
the current PR state, and displays detailed information about what has changed.
|
||||
|
||||
This is useful for:
|
||||
- Testing the incremental review system before running a full review
|
||||
- Understanding what changed since the last review iteration
|
||||
- Verifying that review metadata was properly recorded
|
||||
|
||||
.PARAMETER PullRequestNumber
|
||||
The pull request number to test incremental review detection for.
|
||||
|
||||
.PARAMETER RepositoryOwner
|
||||
The GitHub repository owner. Defaults to "microsoft".
|
||||
|
||||
.PARAMETER RepositoryName
|
||||
The GitHub repository name. Defaults to "PowerToys".
|
||||
|
||||
.OUTPUTS
|
||||
Colored console output displaying:
|
||||
- Current and last reviewed commit SHAs
|
||||
- Whether incremental review is possible
|
||||
- List of new commits since last review
|
||||
- List of changed files with status indicators
|
||||
- Recommended review strategy
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374
|
||||
Tests incremental review detection for PR #42374.
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374 -RepositoryOwner "myorg" -RepositoryName "myrepo"
|
||||
Tests incremental review for a PR in a different repository.
|
||||
|
||||
.NOTES
|
||||
Requires GitHub CLI (gh) to be installed and authenticated.
|
||||
Run 'gh auth login' if not already authenticated.
|
||||
|
||||
Prerequisites:
|
||||
- PR review folder must exist at "Generated Files\prReview\{PRNumber}"
|
||||
- 00-OVERVIEW.md must exist in the review folder
|
||||
- For incremental detection, overview must contain "Last reviewed SHA" metadata
|
||||
|
||||
.LINK
|
||||
https://cli.github.com/
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, HelpMessage = "Pull request number to test")]
|
||||
[int]$PullRequestNumber,
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository owner")]
|
||||
[string]$RepositoryOwner = "microsoft",
|
||||
|
||||
[Parameter(Mandatory = $false, HelpMessage = "Repository name")]
|
||||
[string]$RepositoryName = "PowerToys"
|
||||
)
|
||||
|
||||
# Resolve paths to review folder and overview file
|
||||
$repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$reviewFolderPath = Join-Path $repositoryRoot "Generated Files\prReview\$PullRequestNumber"
|
||||
$overviewFilePath = Join-Path $reviewFolderPath "00-OVERVIEW.md"
|
||||
|
||||
Write-Host "=== Testing Incremental Review for PR #$PullRequestNumber ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Check if review folder exists
|
||||
if (-not (Test-Path $reviewFolderPath)) {
|
||||
Write-Host "❌ Review folder not found: $reviewFolderPath" -ForegroundColor Red
|
||||
Write-Host "This appears to be a new review (iteration 1)" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if overview file exists
|
||||
if (-not (Test-Path $overviewFilePath)) {
|
||||
Write-Host "❌ Overview file not found: $overviewFilePath" -ForegroundColor Red
|
||||
Write-Host "This appears to be an incomplete review" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Read overview file and extract last reviewed SHA
|
||||
Write-Host "📄 Reading overview file..." -ForegroundColor Green
|
||||
$overviewFileContent = Get-Content $overviewFilePath -Raw
|
||||
|
||||
if ($overviewFileContent -match '\*\*Last reviewed SHA:\*\*\s+(\w+)') {
|
||||
$lastReviewedSha = $Matches[1]
|
||||
Write-Host "✅ Found last reviewed SHA: $lastReviewedSha" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ No 'Last reviewed SHA' found in overview - this may be an old format" -ForegroundColor Yellow
|
||||
Write-Host "Proceeding without incremental detection (full review will be needed)" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🔍 Running incremental change detection..." -ForegroundColor Cyan
|
||||
|
||||
# Call the incremental changes detection script
|
||||
$incrementalChangesScriptPath = Join-Path $PSScriptRoot "Get-PrIncrementalChanges.ps1"
|
||||
if (-not (Test-Path $incrementalChangesScriptPath)) {
|
||||
Write-Host "❌ Script not found: $incrementalChangesScriptPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
$analysisResult = & $incrementalChangesScriptPath `
|
||||
-PullRequestNumber $PullRequestNumber `
|
||||
-LastReviewedCommitSha $lastReviewedSha `
|
||||
-RepositoryOwner $RepositoryOwner `
|
||||
-RepositoryName $RepositoryName | ConvertFrom-Json
|
||||
|
||||
# Display analysis results
|
||||
Write-Host ""
|
||||
Write-Host "=== Incremental Review Analysis ===" -ForegroundColor Cyan
|
||||
Write-Host "Current HEAD SHA: $($analysisResult.CurrentHeadSha)" -ForegroundColor White
|
||||
Write-Host "Last reviewed SHA: $($analysisResult.LastReviewedSha)" -ForegroundColor White
|
||||
Write-Host "Base branch: $($analysisResult.BaseRefName)" -ForegroundColor White
|
||||
Write-Host "Head branch: $($analysisResult.HeadRefName)" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "Is incremental? $($analysisResult.IsIncremental)" -ForegroundColor $(if ($analysisResult.IsIncremental) { "Green" } else { "Yellow" })
|
||||
Write-Host "Need full review? $($analysisResult.NeedFullReview)" -ForegroundColor $(if ($analysisResult.NeedFullReview) { "Yellow" } else { "Green" })
|
||||
Write-Host ""
|
||||
Write-Host "Summary: $($analysisResult.Summary)" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Display new commits if any
|
||||
if ($analysisResult.NewCommits -and $analysisResult.NewCommits.Count -gt 0) {
|
||||
Write-Host "📝 New commits ($($analysisResult.NewCommits.Count)):" -ForegroundColor Green
|
||||
foreach ($commit in $analysisResult.NewCommits) {
|
||||
Write-Host " - $($commit.Sha): $($commit.Message)" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Display changed files if any
|
||||
if ($analysisResult.ChangedFiles -and $analysisResult.ChangedFiles.Count -gt 0) {
|
||||
Write-Host "📁 Changed files ($($analysisResult.ChangedFiles.Count)):" -ForegroundColor Green
|
||||
foreach ($file in $analysisResult.ChangedFiles) {
|
||||
$statusDisplayColor = switch ($file.Status) {
|
||||
"added" { "Green" }
|
||||
"removed" { "Red" }
|
||||
"modified" { "Yellow" }
|
||||
"renamed" { "Cyan" }
|
||||
default { "White" }
|
||||
}
|
||||
Write-Host " - [$($file.Status)] $($file.Filename) (+$($file.Additions)/-$($file.Deletions))" -ForegroundColor $statusDisplayColor
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Suggest review strategy based on analysis
|
||||
Write-Host "=== Recommended Review Strategy ===" -ForegroundColor Cyan
|
||||
if ($analysisResult.NeedFullReview) {
|
||||
Write-Host "🔄 Full review recommended" -ForegroundColor Yellow
|
||||
} elseif ($analysisResult.IsIncremental -and ($analysisResult.ChangedFiles.Count -eq 0)) {
|
||||
Write-Host "✅ No changes detected - no review needed" -ForegroundColor Green
|
||||
} elseif ($analysisResult.IsIncremental) {
|
||||
Write-Host "⚡ Incremental review possible - review only changed files" -ForegroundColor Green
|
||||
Write-Host "💡 Consider applying smart step filtering based on file types" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Host "❌ Error running incremental change detection: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
313
.github/review-tools/review-tools.instructions.md
vendored
Normal file
313
.github/review-tools/review-tools.instructions.md
vendored
Normal file
@@ -0,0 +1,313 @@
|
||||
---
|
||||
description: PowerShell scripts for efficient PR reviews in PowerToys repository
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
# PR Review Tools - Reference Guide
|
||||
|
||||
PowerShell scripts to support efficient and incremental pull request reviews in the PowerToys repository.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- PowerShell 7+ (or Windows PowerShell 5.1+)
|
||||
- GitHub CLI (`gh`) installed and authenticated (`gh auth login`)
|
||||
- Access to the PowerToys repository
|
||||
|
||||
### Testing Your Setup
|
||||
|
||||
Run the full test suite (recommended):
|
||||
```powershell
|
||||
cd "d:\PowerToys-00c1\.github\review-tools"
|
||||
.\Run-ReviewToolsTests.ps1
|
||||
```
|
||||
|
||||
Expected: 9-10 tests passing
|
||||
|
||||
### Individual Script Tests
|
||||
|
||||
**Test incremental change detection:**
|
||||
```powershell
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374
|
||||
```
|
||||
Expected: JSON output showing review analysis
|
||||
|
||||
**Preview incremental review:**
|
||||
```powershell
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374
|
||||
```
|
||||
Expected: Analysis showing current vs last reviewed SHA
|
||||
|
||||
**Fetch file content:**
|
||||
```powershell
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "README.md" -GitReference "main"
|
||||
```
|
||||
Expected: README content displayed
|
||||
|
||||
**Get PR file patch:**
|
||||
```powershell
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath ".github/actions/spell-check/expect.txt"
|
||||
```
|
||||
Expected: Unified diff output
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### Get-GitHubRawFile.ps1
|
||||
|
||||
Downloads and displays file content from a GitHub repository at a specific git reference.
|
||||
|
||||
**Purpose:** Retrieve baseline file content for comparison during PR reviews.
|
||||
|
||||
**Parameters:**
|
||||
- `FilePath` (required): Relative path to file in repository
|
||||
- `GitReference` (optional): Git ref (branch, tag, SHA). Default: "main"
|
||||
- `RepositoryOwner` (optional): Repository owner. Default: "microsoft"
|
||||
- `RepositoryName` (optional): Repository name. Default: "PowerToys"
|
||||
- `ShowLineNumbers` (switch): Prefix each line with line number
|
||||
- `StartLineNumber` (optional): Starting line number when using `-ShowLineNumbers`. Default: 1
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\Get-GitHubRawFile.ps1 -FilePath "src/runner/main.cpp" -GitReference "main" -ShowLineNumbers
|
||||
```
|
||||
|
||||
### Get-GitHubPrFilePatch.ps1
|
||||
|
||||
Fetches the unified diff (patch) for a specific file in a pull request.
|
||||
|
||||
**Purpose:** Get the exact changes made to a file in a PR for detailed review.
|
||||
|
||||
**Parameters:**
|
||||
- `PullRequestNumber` (required): Pull request number
|
||||
- `FilePath` (required): Relative path to file in the PR
|
||||
- `RepositoryOwner` (optional): Repository owner. Default: "microsoft"
|
||||
- `RepositoryName` (optional): Repository name. Default: "PowerToys"
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath "src/modules/cmdpal/main.cpp"
|
||||
```
|
||||
|
||||
**Output:** Unified diff showing changes made to the file.
|
||||
|
||||
### Get-PrIncrementalChanges.ps1
|
||||
|
||||
Compares the last reviewed commit with the current PR head to identify incremental changes.
|
||||
|
||||
**Purpose:** Enable efficient incremental reviews by detecting what changed since the last review iteration.
|
||||
|
||||
**Parameters:**
|
||||
- `PullRequestNumber` (required): Pull request number
|
||||
- `LastReviewedCommitSha` (optional): SHA of the commit that was last reviewed. If omitted, assumes first review.
|
||||
- `RepositoryOwner` (optional): Repository owner. Default: "microsoft"
|
||||
- `RepositoryName` (optional): Repository name. Default: "PowerToys"
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123def456"
|
||||
```
|
||||
|
||||
**Output:** JSON object with detailed change analysis:
|
||||
```json
|
||||
{
|
||||
"PullRequestNumber": 42374,
|
||||
"CurrentHeadSha": "xyz789abc123",
|
||||
"LastReviewedSha": "abc123def456",
|
||||
"IsIncremental": true,
|
||||
"NeedFullReview": false,
|
||||
"ChangedFiles": [
|
||||
{
|
||||
"Filename": "src/modules/cmdpal/main.cpp",
|
||||
"Status": "modified",
|
||||
"Additions": 15,
|
||||
"Deletions": 8,
|
||||
"Changes": 23
|
||||
}
|
||||
],
|
||||
"NewCommits": [
|
||||
{
|
||||
"Sha": "def456",
|
||||
"Message": "Fix memory leak",
|
||||
"Author": "John Doe",
|
||||
"Date": "2025-11-07T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"Summary": "Incremental review: 1 new commit(s), 1 file(s) changed since SHA abc123d"
|
||||
}
|
||||
```
|
||||
|
||||
**Scenarios Handled:**
|
||||
- **No LastReviewedCommitSha**: Returns `NeedFullReview: true` (first review)
|
||||
- **SHA matches current HEAD**: Returns empty `ChangedFiles` (no changes)
|
||||
- **Force-push detected**: Returns `NeedFullReview: true` (SHA not in history)
|
||||
- **Incremental changes**: Returns list of changed files and new commits
|
||||
|
||||
### Test-IncrementalReview.ps1
|
||||
|
||||
Helper script to test and preview incremental review detection before running the full review.
|
||||
|
||||
**Purpose:** Validate incremental review functionality and preview what changed.
|
||||
|
||||
**Parameters:**
|
||||
- `PullRequestNumber` (required): Pull request number
|
||||
- `RepositoryOwner` (optional): Repository owner. Default: "microsoft"
|
||||
- `RepositoryName` (optional): Repository name. Default: "PowerToys"
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374
|
||||
```
|
||||
|
||||
**Output:** Colored console output showing:
|
||||
- Current and last reviewed SHAs
|
||||
- Whether incremental review is possible
|
||||
- List of new commits and changed files
|
||||
- Recommended review strategy
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
These scripts integrate with the PR review prompt (`.github/prompts/review-pr.prompt.md`).
|
||||
|
||||
### Typical Review Flow
|
||||
|
||||
1. **Initial Review (Iteration 1)**
|
||||
- Review prompt processes the PR
|
||||
- Creates `Generated Files/prReview/{PR}/00-OVERVIEW.md`
|
||||
- Includes review metadata section with current HEAD SHA
|
||||
|
||||
2. **Subsequent Reviews (Iteration 2+)**
|
||||
- Review prompt reads `00-OVERVIEW.md` to get last reviewed SHA
|
||||
- Calls `Get-PrIncrementalChanges.ps1` to detect what changed
|
||||
- If incremental:
|
||||
- Reviews only changed files
|
||||
- Skips irrelevant review steps (e.g., skip Localization if no `.resx` files changed)
|
||||
- Uses `Get-GitHubPrFilePatch.ps1` to get patches for changed files
|
||||
- Updates `00-OVERVIEW.md` with new SHA and iteration number
|
||||
|
||||
### Manual Testing Workflow
|
||||
|
||||
Preview changes before review:
|
||||
```powershell
|
||||
# Check what changed in PR #42374 since last review
|
||||
.\Test-IncrementalReview.ps1 -PullRequestNumber 42374
|
||||
|
||||
# Get incremental changes programmatically
|
||||
$changes = .\Get-PrIncrementalChanges.ps1 -PullRequestNumber 42374 -LastReviewedCommitSha "abc123" | ConvertFrom-Json
|
||||
|
||||
if (-not $changes.NeedFullReview) {
|
||||
Write-Host "Only need to review $($changes.ChangedFiles.Count) files"
|
||||
|
||||
# Review each changed file
|
||||
foreach ($file in $changes.ChangedFiles) {
|
||||
Write-Host "Reviewing $($file.Filename)..."
|
||||
.\Get-GitHubPrFilePatch.ps1 -PullRequestNumber 42374 -FilePath $file.Filename
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling and Troubleshooting
|
||||
|
||||
### Common Requirements
|
||||
|
||||
All scripts:
|
||||
- Exit with code 1 on error
|
||||
- Write detailed error messages to stderr
|
||||
- Require `gh` CLI to be installed and authenticated
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Error: "gh not found"**
|
||||
- **Solution**: Install GitHub CLI from https://cli.github.com/ and run `gh auth login`
|
||||
|
||||
**Error: "Failed to query GitHub API"**
|
||||
- **Solution**: Verify `gh` authentication with `gh auth status`
|
||||
- **Solution**: Check PR number exists and you have repository access
|
||||
|
||||
**Error: "PR not found"**
|
||||
- **Solution**: Verify the PR number is correct and still exists
|
||||
- **Solution**: Ensure repository owner and name are correct
|
||||
|
||||
**Error: "SHA not found" or "Force-push detected"**
|
||||
- **Explanation**: Last reviewed SHA no longer exists in branch history (force-push occurred)
|
||||
- **Solution**: A full review is required; incremental review not possible
|
||||
|
||||
**Tests show "FAIL" but functionality works**
|
||||
- **Explanation**: Some tests may show exit code failures even when logic is correct
|
||||
- **Solution**: Check test output message - if it says "Correctly detected", functionality is working
|
||||
|
||||
**Error: "Could not find insertion point"**
|
||||
- **Explanation**: Overview file doesn't have expected "**Changed files:**" line
|
||||
- **Solution**: Verify overview file format is correct or regenerate it
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
After setup, verify:
|
||||
- [ ] `Run-ReviewToolsTests.ps1` shows 9+ tests passing
|
||||
- [ ] `Get-PrIncrementalChanges.ps1` returns valid JSON
|
||||
- [ ] `Test-IncrementalReview.ps1` analyzes a PR without errors
|
||||
- [ ] `Get-GitHubRawFile.ps1` downloads files correctly
|
||||
- [ ] `Get-GitHubPrFilePatch.ps1` retrieves patches correctly
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Review Authors
|
||||
|
||||
1. **Test before full review**: Use `Test-IncrementalReview.ps1` to preview changes
|
||||
2. **Check for force-push**: Review the analysis output - force-pushes require full reviews
|
||||
3. **Smart step filtering**: Skip review steps for file types that didn't change
|
||||
|
||||
### For Script Users
|
||||
|
||||
1. **Use absolute paths**: When specifying folders, use absolute paths to avoid ambiguity
|
||||
2. **Check exit codes**: Scripts exit with code 1 on error - check `$LASTEXITCODE` in automation
|
||||
3. **Parse JSON output**: Use `ConvertFrom-Json` to work with structured output from `Get-PrIncrementalChanges.ps1`
|
||||
4. **Handle empty results**: Check `ChangedFiles.Count` before iterating
|
||||
|
||||
### Performance Tips
|
||||
|
||||
1. **Batch operations**: When reviewing multiple PRs, collect all PR numbers and process in batch
|
||||
2. **Cache raw files**: Download baseline files once and reuse for multiple comparisons
|
||||
3. **Filter early**: Use incremental detection to skip unnecessary file reviews
|
||||
4. **Parallel processing**: Consider processing independent PRs in parallel
|
||||
|
||||
## Integration with AI Review Systems
|
||||
|
||||
These tools are designed to work with AI-powered review systems:
|
||||
|
||||
1. **Copilot Instructions**: This file serves as reference documentation for GitHub Copilot
|
||||
2. **Structured Output**: JSON output from scripts is easily parsed by AI systems
|
||||
3. **Incremental Intelligence**: AI can focus on changed files for more efficient reviews
|
||||
4. **Metadata Tracking**: Review iterations are tracked for context-aware suggestions
|
||||
|
||||
### Example AI Integration
|
||||
|
||||
```powershell
|
||||
# Get incremental changes
|
||||
$analysis = .\Get-PrIncrementalChanges.ps1 -PullRequestNumber $PR | ConvertFrom-Json
|
||||
|
||||
# Feed to AI review system
|
||||
$reviewPrompt = @"
|
||||
Review the following changed files in PR #$PR:
|
||||
$($analysis.ChangedFiles | ForEach-Object { "- $($_.Filename) ($($_.Status))" } | Out-String)
|
||||
|
||||
Focus on incremental changes only. Previous review was at SHA $($analysis.LastReviewedSha).
|
||||
"@
|
||||
|
||||
# Execute AI review with context
|
||||
Invoke-AIReview -Prompt $reviewPrompt -Files $analysis.ChangedFiles
|
||||
```
|
||||
|
||||
## Support and Further Information
|
||||
|
||||
For detailed script documentation, use PowerShell's help system:
|
||||
```powershell
|
||||
Get-Help .\Get-PrIncrementalChanges.ps1 -Full
|
||||
Get-Help .\Test-IncrementalReview.ps1 -Detailed
|
||||
```
|
||||
|
||||
Related documentation:
|
||||
- `.github/prompts/review-pr.prompt.md` - Complete review workflow guide
|
||||
- `doc/devdocs/` - PowerToys development documentation
|
||||
- GitHub CLI documentation: https://cli.github.com/manual/
|
||||
|
||||
For issues or questions, refer to the PowerToys contribution guidelines.
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -358,4 +358,4 @@ src/common/Telemetry/*.etl
|
||||
/src/settings-ui/Settings.UI/Assets/Settings/search.index.json
|
||||
|
||||
# PowerToysInstaller Build Temp Files
|
||||
installer/*/*.wxs.bk
|
||||
installer/*/*.wxs.bk
|
||||
|
||||
@@ -235,6 +235,14 @@
|
||||
|
||||
"PowerToys.CmdPalModuleInterface.dll",
|
||||
"CmdPalKeyboardService.dll",
|
||||
"PowerToys.ModuleContracts.dll",
|
||||
"Awake.ModuleServices.dll",
|
||||
"ColorPicker.ModuleServices.dll",
|
||||
"Workspaces.ModuleServices.dll",
|
||||
"Microsoft.CommandPalette.Extensions.dll",
|
||||
"Microsoft.CommandPalette.Extensions.Toolkit.dll",
|
||||
"Microsoft.CmdPal.Ext.PowerToys.dll",
|
||||
"Microsoft.CmdPal.Ext.PowerToys.exe",
|
||||
"*Microsoft.CmdPal.UI_*.msix",
|
||||
|
||||
"PowerToys.DSC.dll",
|
||||
@@ -358,9 +366,13 @@
|
||||
"boost_regex-vc143-mt-x32-1_87.dll",
|
||||
"boost_regex-vc143-mt-x64-1_87.dll",
|
||||
|
||||
"Microsoft.ML.OnnxRuntime.dll",
|
||||
|
||||
"UnitsNet.dll",
|
||||
"UtfUnknown.dll",
|
||||
"Wpf.Ui.dll"
|
||||
"Wpf.Ui.dll",
|
||||
"Shmuelie.WinRTServer.dll",
|
||||
"ToolGood.Words.Pinyin.dll"
|
||||
],
|
||||
"SigningInfo": {
|
||||
"Operations": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Param(
|
||||
# Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
|
||||
[Parameter(Mandatory=$False,Position=1)]
|
||||
[string]$winAppSdkVersionNumber = "1.7",
|
||||
[string]$winAppSdkVersionNumber = "1.8",
|
||||
|
||||
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
|
||||
[Parameter(Mandatory=$False,Position=2)]
|
||||
@@ -16,32 +16,7 @@ Param(
|
||||
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
)
|
||||
|
||||
function Update-NugetConfig {
|
||||
param (
|
||||
[string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
|
||||
)
|
||||
|
||||
Write-Host "Updating nuget.config file"
|
||||
[xml]$xml = Get-Content -Path $filePath
|
||||
|
||||
# Add localpackages source into nuget.config
|
||||
$packageSourcesNode = $xml.configuration.packageSources
|
||||
$addNode = $xml.CreateElement("add")
|
||||
$addNode.SetAttribute("key", "localpackages")
|
||||
$addNode.SetAttribute("value", "localpackages")
|
||||
$packageSourcesNode.AppendChild($addNode) | Out-Null
|
||||
|
||||
# Remove <packageSourceMapping> tag and its content
|
||||
$packageSourceMappingNode = $xml.configuration.packageSourceMapping
|
||||
if ($packageSourceMappingNode) {
|
||||
$xml.configuration.RemoveChild($packageSourceMappingNode) | Out-Null
|
||||
}
|
||||
|
||||
# print nuget.config after modification
|
||||
$xml.OuterXml
|
||||
# Save the modified nuget.config file
|
||||
$xml.Save($filePath)
|
||||
}
|
||||
|
||||
function Read-FileWithEncoding {
|
||||
param (
|
||||
@@ -71,6 +46,132 @@ function Write-FileWithEncoding {
|
||||
$writer.Close()
|
||||
}
|
||||
|
||||
|
||||
function Add-NuGetSourceAndMapping {
|
||||
param (
|
||||
[xml]$Xml,
|
||||
[string]$Key,
|
||||
[string]$Value,
|
||||
[string[]]$Patterns
|
||||
)
|
||||
|
||||
# Ensure packageSources exists
|
||||
if (-not $Xml.configuration.packageSources) {
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSources")) | Out-Null
|
||||
}
|
||||
$sources = $Xml.configuration.packageSources
|
||||
|
||||
# Add/Update Source
|
||||
$sourceNode = $sources.SelectSingleNode("add[@key='$Key']")
|
||||
if (-not $sourceNode) {
|
||||
$sourceNode = $Xml.CreateElement("add")
|
||||
$sourceNode.SetAttribute("key", $Key)
|
||||
$sources.AppendChild($sourceNode) | Out-Null
|
||||
}
|
||||
$sourceNode.SetAttribute("value", $Value)
|
||||
|
||||
# Ensure packageSourceMapping exists
|
||||
if (-not $Xml.configuration.packageSourceMapping) {
|
||||
$Xml.configuration.AppendChild($Xml.CreateElement("packageSourceMapping")) | Out-Null
|
||||
}
|
||||
$mapping = $Xml.configuration.packageSourceMapping
|
||||
|
||||
# Remove invalid packageSource nodes (missing key or empty key)
|
||||
$invalidNodes = $mapping.SelectNodes("packageSource[not(@key) or @key='']")
|
||||
if ($invalidNodes) {
|
||||
foreach ($node in $invalidNodes) {
|
||||
$mapping.RemoveChild($node) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Add/Update Mapping Source
|
||||
$mappingSource = $mapping.SelectSingleNode("packageSource[@key='$Key']")
|
||||
if (-not $mappingSource) {
|
||||
$mappingSource = $Xml.CreateElement("packageSource")
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
# Insert at top for priority
|
||||
if ($mapping.HasChildNodes) {
|
||||
$mapping.InsertBefore($mappingSource, $mapping.FirstChild) | Out-Null
|
||||
} else {
|
||||
$mapping.AppendChild($mappingSource) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Double check and force attribute
|
||||
if (-not $mappingSource.HasAttribute("key")) {
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
}
|
||||
|
||||
# Update Patterns
|
||||
# RemoveAll() removes all child nodes AND attributes, so we must re-set the key afterwards
|
||||
$mappingSource.RemoveAll()
|
||||
$mappingSource.SetAttribute("key", $Key)
|
||||
|
||||
foreach ($pattern in $Patterns) {
|
||||
$pkg = $Xml.CreateElement("package")
|
||||
$pkg.SetAttribute("pattern", $pattern)
|
||||
$mappingSource.AppendChild($pkg) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-WinAppSdkSplitDependencies {
|
||||
Write-Host "Version $WinAppSDKVersion detected. Resolving split dependencies..."
|
||||
$installDir = Join-Path $rootPath "localpackages\output"
|
||||
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
|
||||
|
||||
# Create a temporary nuget.config to avoid interference from the repo's config
|
||||
$tempConfig = Join-Path $env:TEMP "nuget_$(Get-Random).config"
|
||||
Set-Content -Path $tempConfig -Value "<?xml version='1.0' encoding='utf-8'?><configuration><packageSources><clear /><add key='TempSource' value='$sourceLink' /></packageSources></configuration>"
|
||||
|
||||
try {
|
||||
# Extract BuildTools version from Directory.Packages.props to ensure we have the required version
|
||||
$dirPackagesProps = Join-Path $rootPath "Directory.Packages.props"
|
||||
if (Test-Path $dirPackagesProps) {
|
||||
$propsContent = Get-Content $dirPackagesProps -Raw
|
||||
if ($propsContent -match '<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="([^"]+)"') {
|
||||
$buildToolsVersion = $Matches[1]
|
||||
Write-Host "Downloading Microsoft.Windows.SDK.BuildTools version $buildToolsVersion..."
|
||||
$nugetArgsBuildTools = "install Microsoft.Windows.SDK.BuildTools -Version $buildToolsVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
|
||||
Invoke-Expression "nuget $nugetArgsBuildTools" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Download package to inspect nuspec and keep it for the build
|
||||
$nugetArgs = "install Microsoft.WindowsAppSDK -Version $WinAppSDKVersion -ConfigFile $tempConfig -OutputDirectory $installDir -NonInteractive -NoCache"
|
||||
Invoke-Expression "nuget $nugetArgs" | Out-Null
|
||||
|
||||
# Parse dependencies from the installed folders
|
||||
# Folder structure is typically {PackageId}.{Version}
|
||||
$directories = Get-ChildItem -Path $installDir -Directory
|
||||
$allLocalPackages = @()
|
||||
foreach ($dir in $directories) {
|
||||
# Match any package pattern: PackageId.Version
|
||||
if ($dir.Name -match "^(.+?)\.(\d+\..*)$") {
|
||||
$pkgId = $Matches[1]
|
||||
$pkgVer = $Matches[2]
|
||||
$allLocalPackages += $pkgId
|
||||
|
||||
$packageVersions[$pkgId] = $pkgVer
|
||||
Write-Host "Found dependency: $pkgId = $pkgVer"
|
||||
}
|
||||
}
|
||||
|
||||
# Update repo's nuget.config to use localpackages
|
||||
$nugetConfig = Join-Path $rootPath "nuget.config"
|
||||
$configData = Read-FileWithEncoding -Path $nugetConfig
|
||||
[xml]$xml = $configData.Content
|
||||
|
||||
Add-NuGetSourceAndMapping -Xml $xml -Key "localpackages" -Value $installDir -Patterns $allLocalPackages
|
||||
|
||||
$xml.Save($nugetConfig)
|
||||
Write-Host "Updated nuget.config with localpackages mapping."
|
||||
} catch {
|
||||
Write-Warning "Failed to resolve dependencies: $_"
|
||||
} finally {
|
||||
Remove-Item $tempConfig -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Execute nuget list and capture the output
|
||||
if ($useExperimentalVersion) {
|
||||
# The nuget list for experimental versions will cost more time
|
||||
@@ -112,56 +213,36 @@ if ($latestVersion) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Update packages.config files
|
||||
Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
# Resolve dependencies for 1.8+
|
||||
$packageVersions = @{ "Microsoft.WindowsAppSDK" = $WinAppSDKVersion }
|
||||
|
||||
Resolve-WinAppSdkSplitDependencies
|
||||
|
||||
# Update Directory.Packages.props file
|
||||
Get-ChildItem -Path $rootPath -Recurse "Directory.Packages.props" | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
|
||||
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
$isModified = $false
|
||||
|
||||
foreach ($pkgId in $packageVersions.Keys) {
|
||||
$ver = $packageVersions[$pkgId]
|
||||
# Escape dots in package ID for regex
|
||||
$pkgIdRegex = $pkgId -replace '\.', '\.'
|
||||
|
||||
$newVersionString = "<PackageVersion Include=""$pkgId"" Version=""$ver"" />"
|
||||
$oldVersionString = "<PackageVersion Include=""$pkgIdRegex"" Version=""[-.0-9a-zA-Z]*"" />"
|
||||
|
||||
if ($content -match "<PackageVersion Include=""$pkgIdRegex""") {
|
||||
# Update existing package
|
||||
if ($content -notmatch [regex]::Escape($newVersionString)) {
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
$isModified = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isModified) {
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .vcxproj files
|
||||
Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match '\\Microsoft.WindowsAppSDK.') {
|
||||
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion
|
||||
$oldVersionString = '\\Microsoft.WindowsAppSDK.(?=[-.0-9a-zA-Z]*\d)[-.0-9a-zA-Z]*' #positive lookahead for at least a digit
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .csproj files
|
||||
Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
Update-NugetConfig
|
||||
|
||||
@@ -19,7 +19,7 @@ parameters:
|
||||
- name: enableMsBuildCaching
|
||||
type: boolean
|
||||
displayName: "Enable MSBuild Caching"
|
||||
default: true
|
||||
default: false
|
||||
- name: runTests
|
||||
type: boolean
|
||||
displayName: "Run Tests"
|
||||
@@ -33,7 +33,7 @@ parameters:
|
||||
default: true
|
||||
- name: winAppSDKVersionNumber
|
||||
type: string
|
||||
default: 1.7
|
||||
default: 1.8
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
MSBuildMainBuildTargets: Build
|
||||
${{ insert }}: ${{ parameters.variables }}
|
||||
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
|
||||
RestoreAdditionalProjectSourcesArg: '/p:RestoreAdditionalProjectSources="$(Build.SourcesDirectory)\localpackages\NugetPackages"'
|
||||
RestoreAdditionalProjectSourcesArg: '/p:RestoreAdditionalProjectSources="$(Build.SourcesDirectory)\localpackages\NugetPackages" /p:IgnoreExperimentalWarnings=true'
|
||||
${{ else }}:
|
||||
RestoreAdditionalProjectSourcesArg: ''
|
||||
displayName: Build
|
||||
@@ -624,4 +624,4 @@ jobs:
|
||||
- publish: $(JobOutputDirectory)
|
||||
artifact: $(JobOutputArtifactName)-failure-$(System.JobAttempt)
|
||||
displayName: Publish failure logs
|
||||
condition: or(failed(), canceled())
|
||||
condition: or(failed(), canceled())
|
||||
@@ -63,3 +63,20 @@ stages:
|
||||
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
timeoutInMinutes: 90
|
||||
|
||||
- stage: Build_SDK
|
||||
displayName: Build Command Palette Toolkit SDK
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- template: job-build-sdk.yml
|
||||
parameters:
|
||||
pool:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
buildConfigurations: [Release]
|
||||
official: false
|
||||
codeSign: false
|
||||
|
||||
@@ -19,48 +19,20 @@ steps:
|
||||
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
|
||||
-rootPath "$(build.sourcesdirectory)"
|
||||
|
||||
- script: echo $(WinAppSDKVersion)
|
||||
displayName: 'Display WinAppSDK Version Found'
|
||||
# - task: NuGetCommand@2
|
||||
# displayName: 'Restore NuGet packages (slnx)'
|
||||
# inputs:
|
||||
# command: 'restore'
|
||||
# feedsToUse: 'config'
|
||||
# nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
# restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
# includeNuGetOrg: false
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download WindowsAppSDK'
|
||||
inputs:
|
||||
buildType: 'specific'
|
||||
project: '55e8140e-57ac-4e5f-8f9c-c7c15b51929d'
|
||||
definition: '104083'
|
||||
buildVersionToDownload: 'latestFromBranch'
|
||||
branchName: 'refs/heads/release/${{ parameters.versionNumber }}-stable'
|
||||
artifactName: 'WindowsAppSDK_Nuget_And_MSIX'
|
||||
targetPath: '$(Build.SourcesDirectory)\localpackages'
|
||||
|
||||
- script: dir $(Build.SourcesDirectory)\localpackages\NugetPackages
|
||||
displayName: 'List downloaded packages'
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Install WindowsAppSDK'
|
||||
inputs:
|
||||
command: 'custom'
|
||||
arguments: >
|
||||
install "Microsoft.WindowsAppSDK"
|
||||
-Source "$(Build.SourcesDirectory)\localpackages\NugetPackages"
|
||||
-Version "$(WinAppSDKVersion)"
|
||||
-OutputDirectory "$(Build.SourcesDirectory)\localpackages\output"
|
||||
-FallbackSource "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore NuGet packages'
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Restore NuGet packages (dotnet)'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
projects: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
restoreSolution: '$(build.sourcesdirectory)\**\*.sln'
|
||||
includeNuGetOrg: false
|
||||
|
||||
- task: NuGetCommand@2
|
||||
displayName: 'Restore NuGet packages (slnx)'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
|
||||
restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
|
||||
includeNuGetOrg: false
|
||||
workingDirectory: '$(build.sourcesdirectory)'
|
||||
|
||||
@@ -104,4 +104,4 @@ if ($totalFailure -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
exit 0
|
||||
@@ -42,6 +42,11 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<!-- Make angle-bracket includes external and turn off code analysis for them -->
|
||||
<TreatAngleIncludeAsExternal>true</TreatAngleIncludeAsExternal>
|
||||
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
|
||||
<DisableAnalyzeExternal>true</DisableAnalyzeExternal>
|
||||
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
@@ -111,13 +116,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Debug/Release props -->
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'"
|
||||
Label="Configuration">
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'"
|
||||
Label="Configuration">
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -262,6 +262,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Command Palette
|
||||
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
@@ -315,6 +316,14 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.CmdPalProcessStarted</td>
|
||||
<td>Triggered when the Command Palette process is started.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_ExtensionInvoked</td>
|
||||
<td>Tracks extension usage including extension ID, command details, success status, and execution time.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdPal_SessionDuration</td>
|
||||
<td>Logs session metrics from launch to dismissal including duration, commands executed, pages visited, search queries, navigation depth, and errors.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Crop And Lock
|
||||
|
||||
@@ -8,4 +8,24 @@
|
||||
<PropertyGroup Label="ManifestToolOverride">
|
||||
<ManifestTool Condition="Exists('$(WindowsSdkDir)bin\x64\mt.exe')">$(WindowsSdkDir)bin\x64\mt.exe</ManifestTool>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Auto-restore NuGet for native vcxproj (PackageReference) when building inside VS -->
|
||||
<Target Name="EnsureNuGetRestoreForVcxproj" BeforeTargets="PrepareForBuild" Condition="
|
||||
'$(BuildingInsideVisualStudio)' == 'true'
|
||||
and '$(DesignTimeBuild)' != 'true'
|
||||
and '$(RestoreInProgress)' != 'true'
|
||||
and '$(MSBuildProjectExtension)' == '.vcxproj'
|
||||
and '$(RestoreProjectStyle)' == 'PackageReference'
|
||||
and '$(MSBuildProjectExtensionsPath)' != ''
|
||||
and !Exists('$(MSBuildProjectExtensionsPath)project.assets.json')
|
||||
">
|
||||
|
||||
<Message Importance="normal" Text="NuGet assets missing for $(MSBuildProjectName); running Restore...; IntDir=$(IntDir); BaseIntermediateOutputPath=$(BaseIntermediateOutputPath)" />
|
||||
|
||||
<MSBuild Projects="$(MSBuildProjectFullPath)" Targets="Restore" Properties="RestoreInProgress=true" BuildInParallel="false" />
|
||||
</Target>
|
||||
|
||||
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
|
||||
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -7,6 +7,8 @@
|
||||
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
||||
<PackageVersion Include="boost" Version="1.87.0" TargetFramework="native" />
|
||||
<PackageVersion Include="boost_regex-vc143" Version="1.87.0" TargetFramework="native" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
@@ -35,6 +37,7 @@
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.5.250829002" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
@@ -70,10 +73,13 @@
|
||||
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.251104000" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.39" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.251106002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.WinUI" Version="1.8.251105000" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
@@ -90,6 +96,7 @@
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.37.2" />
|
||||
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
|
||||
<!-- Don't update SkiaSharp.Views.WinUI to version 3.* branch as this brakes the HexBox control in Registry Preview. -->
|
||||
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="2.88.9" />
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
|
||||
@@ -112,6 +119,7 @@
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.11" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />
|
||||
@@ -140,4 +148,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1560,6 +1560,7 @@ SOFTWARE.
|
||||
- ReverseMarkdown
|
||||
- ScipBe.Common.Office.OneNote
|
||||
- SharpCompress
|
||||
- Shmuelie.WinRTServer
|
||||
- SkiaSharp.Views.WinUI
|
||||
- StreamJsonRpc
|
||||
- StyleCop.Analyzers
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/common/PowerToys.ModuleContracts/PowerToys.ModuleContracts.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/common/SettingsAPI/SettingsAPI.vcxproj" Id="6955446d-23f7-4023-9bb3-8657f904af99" />
|
||||
<Project Path="src/common/Themes/Themes.vcxproj" Id="98537082-0fdb-40de-abd8-0dc5a4269bab" />
|
||||
<Project Path="src/common/UITestAutomation/UITestAutomation.csproj">
|
||||
@@ -156,6 +160,10 @@
|
||||
<Project Path="src/modules/alwaysontop/AlwaysOnTopModuleInterface/AlwaysOnTopModuleInterface.vcxproj" Id="48a0a19e-a0be-4256-acf8-cc3b80291af9" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/awake/">
|
||||
<Project Path="src/modules/awake/Awake.ModuleServices/Awake.ModuleServices.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/awake/Awake/Awake.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
@@ -166,6 +174,10 @@
|
||||
<Project Path="src/modules/cmdNotFound/CmdNotFoundModuleInterface/CmdNotFoundModuleInterface.vcxproj" Id="0014d652-901f-4456-8d65-06fc5f997fb0" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/colorpicker/">
|
||||
<Project Path="src/modules/colorPicker/ColorPicker.ModuleServices/ColorPicker.ModuleServices.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/colorPicker/ColorPicker/ColorPicker.vcxproj" Id="655c9af2-18d3-4da6-80e4-85504a7722ba">
|
||||
<BuildDependency Project="src/common/logger/logger.vcxproj" />
|
||||
</Project>
|
||||
@@ -206,6 +218,11 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
@@ -356,34 +373,6 @@
|
||||
<Project Path="src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj" Id="f5e1146e-b7b3-4e11-85fd-270a500bd78c" />
|
||||
<Project Path="src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj" Id="3157fa75-86cf-4ee2-8f62-c43f776493c6" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/Deux/" />
|
||||
<Folder Name="/modules/Deux/SDK/">
|
||||
<Project Path="src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions.Toolkit/Microsoft.CommandPalette.Extensions.Toolkit.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/Deux/SDK/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj" Id="7997dad4-31d6-496b-95db-6c028d699370" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/Deux/UI/">
|
||||
<Project Path="src/modules/Deux/UI/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj" Id="fc192bce-bfbb-40f5-8c2d-9858de12c31f" />
|
||||
<Project Path="src/modules/Deux/UI/Microsoft.CommandPalette.UI.Models/Microsoft.CommandPalette.UI.Models.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/Deux/UI/Microsoft.CommandPalette.UI.Services/Microsoft.CommandPalette.UI.Services.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/Deux/UI/Microsoft.CommandPalette.UI.ViewModels/Microsoft.CommandPalette.UI.ViewModels.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/Deux/UI/Microsoft.CommandPalette.UI/Microsoft.CommandPalette.UI.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/EnvironmentVariables/">
|
||||
<Project Path="src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
@@ -960,6 +949,10 @@
|
||||
<Project Path="src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj" Id="2d604c07-51fc-46bb-9eb7-75aecc7f5e81" />
|
||||
</Folder>
|
||||
<Folder Name="/modules/Workspaces/">
|
||||
<Project Path="src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/Workspaces/WorkspacesCsharpLibrary/WorkspacesCsharpLibrary.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
@@ -7,11 +7,18 @@
|
||||
|
||||
<Fragment>
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
<Component Id="Microsoft_CommandPalette_Extensions_winmd" Guid="304AD25A-A986-4058-940E-61DB79EBD78C" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="Microsoft_CommandPalette_Extensions_winmd" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="Microsoft.CommandPalette.Extensions.winmd" Source="$(var.BinDir)Microsoft.CommandPalette.Extensions.winmd" />
|
||||
</Component>
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--BaseApplicationsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="BaseApplicationsComponentGroup">
|
||||
<ComponentRef Id="Microsoft_CommandPalette_Extensions_winmd" />
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
|
||||
@@ -173,4 +173,4 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
<Target Name="Restore" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
|
||||
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10"
|
||||
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai">
|
||||
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
||||
IgnorableNamespaces="uap uap2 uap3 rescap desktop uap10 systemai com">
|
||||
<Identity
|
||||
Name="Microsoft.PowerToys.SparseApp"
|
||||
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
@@ -30,6 +31,7 @@
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19000.0" MaxVersionTested="10.0.26226.0" />
|
||||
</Dependencies>
|
||||
<Capabilities>
|
||||
<Capability Name="internetClient" />
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<systemai:Capability Name="systemAIModels"/>
|
||||
<rescap:Capability Name="unvirtualizedResources"/>
|
||||
@@ -66,5 +68,42 @@
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.CmdPalExtension" Executable="Microsoft.CmdPal.Ext.PowerToys.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
<uap:VisualElements
|
||||
DisplayName="PowerToys.CommandPaletteExtension"
|
||||
Description="PowerToys Command Palette Extension"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
<Extensions>
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:ExeServer Executable="Microsoft.CmdPal.Ext.PowerToys.exe" Arguments="-RegisterProcessAsComServer" DisplayName="PowerToys Command Palette Extension">
|
||||
<com:Class Id="7EC02C7D-8F98-4A2E-9F23-B58C2C2F2B17" DisplayName="PowerToys Command Palette Extension" />
|
||||
</com:ExeServer>
|
||||
</com:ComServer>
|
||||
</com:Extension>
|
||||
<uap3:Extension Category="windows.appExtension">
|
||||
<uap3:AppExtension Name="com.microsoft.commandpalette"
|
||||
Id="PowerToys"
|
||||
PublicFolder="Public"
|
||||
DisplayName="PowerToys"
|
||||
Description="Surface PowerToys commands inside Command Palette">
|
||||
<uap3:Properties>
|
||||
<CmdPalProvider>
|
||||
<Activation>
|
||||
<CreateInstance ClassId="7EC02C7D-8F98-4A2E-9F23-B58C2C2F2B17" />
|
||||
</Activation>
|
||||
<SupportedInterfaces>
|
||||
<Commands/>
|
||||
</SupportedInterfaces>
|
||||
</CmdPalProvider>
|
||||
</uap3:Properties>
|
||||
</uap3:AppExtension>
|
||||
</uap3:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
</Package>
|
||||
</Package>
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace Common.UI
|
||||
{
|
||||
@@ -120,28 +122,33 @@ namespace Common.UI
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenSettings(SettingsWindow window, bool mainExecutableIsOnTheParentFolder)
|
||||
// What about debug build? Should also consider debug build, maybe tray window message?
|
||||
public static void OpenSettings(SettingsWindow window)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directoryPath = System.AppContext.BaseDirectory;
|
||||
if (mainExecutableIsOnTheParentFolder)
|
||||
var exePath = Path.Combine(
|
||||
PowerToysPathResolver.GetPowerToysInstallPath(),
|
||||
"PowerToys.exe");
|
||||
|
||||
if (exePath == null || !File.Exists(exePath))
|
||||
{
|
||||
// Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
|
||||
directoryPath = Path.Combine(directoryPath, "..");
|
||||
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
|
||||
}
|
||||
else
|
||||
{
|
||||
// PowerToys.exe is in the same path as the application.
|
||||
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
|
||||
Logger.LogError($"Failed to find powertoys exe path, {exePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
|
||||
var args = "--open-settings=" + SettingsWindowNameToString(window);
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
});
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO(stefan): Log exception once unified logging is implemented
|
||||
Logger.LogError(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
168
src/common/ManagedCommon/PowerToysPathResolver.cs
Normal file
168
src/common/ManagedCommon/PowerToysPathResolver.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public class PowerToysPathResolver
|
||||
{
|
||||
private const string PowerToysRegistryKey = @"Software\Classes\powertoys";
|
||||
private const string PowerToysExe = "PowerToys.exe";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the PowerToys installation path by checking registry entries
|
||||
/// </summary>
|
||||
/// <returns>The path to PowerToys installation directory, or null if not found</returns>
|
||||
public static string GetPowerToysInstallPath()
|
||||
{
|
||||
#if DEBUG
|
||||
// In debug builds, resolve directly from the running process (no installer/registry involved).
|
||||
return GetPathFromCurrentProcess();
|
||||
#else
|
||||
// Try to get path from Per-User installation first
|
||||
string path = GetPathFromRegistry(RegistryHive.CurrentUser);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
// Fall back to Per-Machine installation
|
||||
path = GetPathFromRegistry(RegistryHive.LocalMachine);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
private static string GetPathFromRegistry(RegistryHive hive)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var baseKey = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64);
|
||||
|
||||
// First try to get path from the powertoys protocol registration
|
||||
string path = GetPathFromProtocolRegistration(baseKey);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore registry access errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetPathFromProtocolRegistration(RegistryKey baseKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = baseKey.OpenSubKey($@"{PowerToysRegistryKey}\shell\open\command");
|
||||
|
||||
if (key != null)
|
||||
{
|
||||
string command = key.GetValue(string.Empty)?.ToString();
|
||||
if (!string.IsNullOrEmpty(command))
|
||||
{
|
||||
// Parse command like: "C:\Program Files\PowerToys\PowerToys.exe" "%1"
|
||||
return ExtractPathFromCommand(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore registry access errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetPathFromCurrentProcess()
|
||||
{
|
||||
try
|
||||
{
|
||||
// If we're running inside PowerToys.exe (dev/debug builds), use the executable location.
|
||||
var processPath = Process.GetCurrentProcess().MainModule?.FileName;
|
||||
if (!string.IsNullOrEmpty(processPath))
|
||||
{
|
||||
var processDir = Path.GetDirectoryName(processPath);
|
||||
if (!string.IsNullOrEmpty(processDir) && File.Exists(Path.Combine(processDir, PowerToysExe)))
|
||||
{
|
||||
return processDir;
|
||||
}
|
||||
}
|
||||
|
||||
// As a fallback, walk up from AppContext.BaseDirectory to find PowerToys.exe.
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory != null)
|
||||
{
|
||||
var candidate = Path.Combine(directory.FullName, PowerToysExe);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return directory.FullName;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore reflection/process permission errors; caller will see null and handle accordingly.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ExtractPathFromCommand(string command)
|
||||
{
|
||||
if (string.IsNullOrEmpty(command))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Handle quoted paths: "C:\Program Files\PowerToys\PowerToys.exe" "%1"
|
||||
if (command.StartsWith('\"'))
|
||||
{
|
||||
int endQuote = command.IndexOf('\"', 1);
|
||||
if (endQuote > 1)
|
||||
{
|
||||
string exePath = command.Substring(1, endQuote - 1);
|
||||
if (File.Exists(exePath))
|
||||
{
|
||||
return Path.GetDirectoryName(exePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle unquoted paths (less common)
|
||||
string[] parts = command.Split(' ');
|
||||
if (parts.Length > 0 && File.Exists(parts[0]))
|
||||
{
|
||||
return Path.GetDirectoryName(parts[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore path parsing errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/common/PowerToys.ModuleContracts/IModuleService.cs
Normal file
47
src/common/PowerToys.ModuleContracts/IModuleService.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Common.UI;
|
||||
|
||||
namespace PowerToys.ModuleContracts;
|
||||
|
||||
/// <summary>
|
||||
/// Base contract for PowerToys modules exposed to the Command Palette.
|
||||
/// </summary>
|
||||
public interface IModuleService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets module identifier (e.g., Workspaces, Awake).
|
||||
/// </summary>
|
||||
string Key { get; }
|
||||
|
||||
Task<OperationResult> LaunchAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<OperationResult> OpenSettingsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper base to reduce duplication for simple modules.
|
||||
/// </summary>
|
||||
public abstract class ModuleServiceBase : IModuleService
|
||||
{
|
||||
public abstract string Key { get; }
|
||||
|
||||
protected abstract SettingsDeepLink.SettingsWindow SettingsWindow { get; }
|
||||
|
||||
public abstract Task<OperationResult> LaunchAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
public virtual Task<OperationResult> OpenSettingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsWindow);
|
||||
return Task.FromResult(OperationResult.Ok());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(OperationResult.Fail($"Failed to open settings for {Key}: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/common/PowerToys.ModuleContracts/OperationResult.cs
Normal file
30
src/common/PowerToys.ModuleContracts/OperationResult.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerToys.ModuleContracts;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight result type for module operations.
|
||||
/// </summary>
|
||||
public readonly record struct OperationResult(bool Success, string? Error = null)
|
||||
{
|
||||
public static OperationResult Ok() => new(true, null);
|
||||
|
||||
public static OperationResult Fail(string error) => new(false, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result type with a payload.
|
||||
/// </summary>
|
||||
public readonly record struct OperationResult<T>(bool Success, T? Value, string? Error = null);
|
||||
|
||||
/// <summary>
|
||||
/// Factory helpers for creating operation results.
|
||||
/// </summary>
|
||||
public static class OperationResults
|
||||
{
|
||||
public static OperationResult<T> Ok<T>(T value) => new(true, value, null);
|
||||
|
||||
public static OperationResult<T> Fail<T>(string error) => new(false, default, error);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common.UI\Common.UI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -75,10 +75,62 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE;
|
||||
}
|
||||
hstring Constants::AdvancedPasteShowUIEvent()
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT;
|
||||
}
|
||||
hstring Constants::AdvancedPasteTerminateAppMessage()
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_TERMINATE_APP_MESSAGE;
|
||||
}
|
||||
hstring Constants::AlwaysOnTopPinEvent()
|
||||
{
|
||||
return CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT;
|
||||
}
|
||||
hstring Constants::FindMyMouseTriggerEvent()
|
||||
{
|
||||
return CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT;
|
||||
}
|
||||
hstring Constants::MouseHighlighterTriggerEvent()
|
||||
{
|
||||
return CommonSharedConstants::MOUSE_HIGHLIGHTER_TRIGGER_EVENT;
|
||||
}
|
||||
hstring Constants::MouseCrosshairsTriggerEvent()
|
||||
{
|
||||
return CommonSharedConstants::MOUSE_CROSSHAIRS_TRIGGER_EVENT;
|
||||
}
|
||||
hstring Constants::CursorWrapTriggerEvent()
|
||||
{
|
||||
return CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT;
|
||||
}
|
||||
hstring Constants::LightSwitchToggleEvent()
|
||||
{
|
||||
return CommonSharedConstants::LIGHTSWITCH_TOGGLE_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItZoomEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_ZOOM_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItDrawEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_DRAW_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItBreakEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_BREAK_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItLiveZoomEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_LIVEZOOM_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItSnipEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_SNIP_EVENT;
|
||||
}
|
||||
hstring Constants::ZoomItRecordEvent()
|
||||
{
|
||||
return CommonSharedConstants::ZOOMIT_RECORD_EVENT;
|
||||
}
|
||||
hstring Constants::ShowPowerOCRSharedEvent()
|
||||
{
|
||||
return CommonSharedConstants::SHOW_POWEROCR_SHARED_EVENT;
|
||||
|
||||
@@ -23,6 +23,20 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring AdvancedPasteAdditionalActionMessage();
|
||||
static hstring AdvancedPasteCustomActionMessage();
|
||||
static hstring AdvancedPasteTerminateAppMessage();
|
||||
static hstring AdvancedPasteShowUIEvent();
|
||||
static hstring AlwaysOnTopPinEvent();
|
||||
static hstring MeasureToolTriggerEvent();
|
||||
static hstring FindMyMouseTriggerEvent();
|
||||
static hstring MouseHighlighterTriggerEvent();
|
||||
static hstring MouseCrosshairsTriggerEvent();
|
||||
static hstring CursorWrapTriggerEvent();
|
||||
static hstring LightSwitchToggleEvent();
|
||||
static hstring ZoomItZoomEvent();
|
||||
static hstring ZoomItDrawEvent();
|
||||
static hstring ZoomItBreakEvent();
|
||||
static hstring ZoomItLiveZoomEvent();
|
||||
static hstring ZoomItSnipEvent();
|
||||
static hstring ZoomItRecordEvent();
|
||||
static hstring ShowPowerOCRSharedEvent();
|
||||
static hstring TerminatePowerOCRSharedEvent();
|
||||
static hstring MouseJumpShowPreviewEvent();
|
||||
@@ -33,7 +47,6 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring PowerAccentExitEvent();
|
||||
static hstring ShortcutGuideTriggerEvent();
|
||||
static hstring RegistryPreviewTriggerEvent();
|
||||
static hstring MeasureToolTriggerEvent();
|
||||
static hstring GcodePreviewResizeEvent();
|
||||
static hstring BgcodePreviewResizeEvent();
|
||||
static hstring QoiPreviewResizeEvent();
|
||||
|
||||
@@ -20,6 +20,19 @@ namespace PowerToys
|
||||
static String AdvancedPasteAdditionalActionMessage();
|
||||
static String AdvancedPasteCustomActionMessage();
|
||||
static String AdvancedPasteTerminateAppMessage();
|
||||
static String AdvancedPasteShowUIEvent();
|
||||
static String AlwaysOnTopPinEvent();
|
||||
static String FindMyMouseTriggerEvent();
|
||||
static String MouseHighlighterTriggerEvent();
|
||||
static String MouseCrosshairsTriggerEvent();
|
||||
static String CursorWrapTriggerEvent();
|
||||
static String LightSwitchToggleEvent();
|
||||
static String ZoomItZoomEvent();
|
||||
static String ZoomItDrawEvent();
|
||||
static String ZoomItBreakEvent();
|
||||
static String ZoomItLiveZoomEvent();
|
||||
static String ZoomItSnipEvent();
|
||||
static String ZoomItRecordEvent();
|
||||
static String ShowPowerOCRSharedEvent();
|
||||
static String TerminatePowerOCRSharedEvent();
|
||||
static String MouseJumpShowPreviewEvent();
|
||||
@@ -51,4 +64,4 @@ namespace PowerToys
|
||||
static String ShowCmdPalEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace CommonSharedConstants
|
||||
const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction";
|
||||
|
||||
const wchar_t ADVANCED_PASTE_TERMINATE_APP_MESSAGE[] = L"TerminateApp";
|
||||
|
||||
const wchar_t ADVANCED_PASTE_SHOW_UI_EVENT[] = L"Local\\PowerToys_AdvancedPaste_ShowUI";
|
||||
|
||||
// Path to the event used to show Color Picker
|
||||
const wchar_t SHOW_COLOR_PICKER_SHARED_EVENT[] = L"Local\\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525";
|
||||
@@ -83,12 +85,21 @@ namespace CommonSharedConstants
|
||||
|
||||
const wchar_t TERMINATE_MOUSE_JUMP_SHARED_EVENT[] = L"Local\\TerminateMouseJumpEvent-252fa337-317f-4c37-a61f-99464c3f9728";
|
||||
|
||||
// Paths to the events used by other Mouse Utilities
|
||||
const wchar_t FIND_MY_MOUSE_TRIGGER_EVENT[] = L"Local\\FindMyMouseTriggerEvent-5a9dc5f4-1c74-4f2f-a66f-1b9b6a2f9b23";
|
||||
const wchar_t MOUSE_HIGHLIGHTER_TRIGGER_EVENT[] = L"Local\\MouseHighlighterTriggerEvent-1e3c9c3d-3fdf-4f9a-9a52-31c9b3c3a8f4";
|
||||
const wchar_t MOUSE_CROSSHAIRS_TRIGGER_EVENT[] = L"Local\\MouseCrosshairsTriggerEvent-0d4c7f92-0a5c-4f5c-b64b-8a2a2f7e0b21";
|
||||
const wchar_t CURSOR_WRAP_TRIGGER_EVENT[] = L"Local\\CursorWrapTriggerEvent-1f8452b5-4e6e-45b3-8b09-13f14a5900c9";
|
||||
|
||||
// Path to the event used by RegistryPreview
|
||||
const wchar_t REGISTRY_PREVIEW_TRIGGER_EVENT[] = L"Local\\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687";
|
||||
|
||||
// Path to the event used by MeasureTool
|
||||
const wchar_t MEASURE_TOOL_TRIGGER_EVENT[] = L"Local\\MeasureToolEvent-3d46745f-09b3-4671-a577-236be7abd199";
|
||||
|
||||
// Path to the event used by LightSwitch
|
||||
const wchar_t LIGHTSWITCH_TOGGLE_EVENT[] = L"Local\\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a";
|
||||
|
||||
// Path to the event used by GcodePreviewHandler
|
||||
const wchar_t GCODE_PREVIEW_RESIZE_EVENT[] = L"Local\\PowerToysGcodePreviewResizeEvent-6ff1f9bd-ccbd-4b24-a79f-40a34fb0317d";
|
||||
|
||||
@@ -130,6 +141,12 @@ namespace CommonSharedConstants
|
||||
// Path to the events used by ZoomIt
|
||||
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
|
||||
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
|
||||
const wchar_t ZOOMIT_ZOOM_EVENT[] = L"Local\\PowerToysZoomIt-ZoomEvent-1e4190d7-94bc-4ad5-adc0-9a8fd07cb393";
|
||||
const wchar_t ZOOMIT_DRAW_EVENT[] = L"Local\\PowerToysZoomIt-DrawEvent-56338997-404d-4549-bd9a-d132b6766975";
|
||||
const wchar_t ZOOMIT_BREAK_EVENT[] = L"Local\\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b";
|
||||
const wchar_t ZOOMIT_LIVEZOOM_EVENT[] = L"Local\\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d";
|
||||
const wchar_t ZOOMIT_SNIP_EVENT[] = L"Local\\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30";
|
||||
const wchar_t ZOOMIT_RECORD_EVENT[] = L"Local\\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512";
|
||||
|
||||
// used from quick access window
|
||||
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
|
||||
|
||||
@@ -3,78 +3,128 @@
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include <windows.h>
|
||||
|
||||
/// <summary>
|
||||
/// A reusable utility class that listens for a named Windows event and invokes a callback when triggered.
|
||||
/// Provides RAII-based resource management for event handles and the listener thread.
|
||||
/// The thread is properly joined on destruction to ensure clean shutdown.
|
||||
/// </summary>
|
||||
class EventWaiter
|
||||
{
|
||||
public:
|
||||
EventWaiter() {}
|
||||
EventWaiter(const std::wstring& name, std::function<void(DWORD)> callback)
|
||||
EventWaiter() = default;
|
||||
|
||||
EventWaiter(const EventWaiter&) = delete;
|
||||
EventWaiter& operator=(const EventWaiter&) = delete;
|
||||
EventWaiter(EventWaiter&&) = delete;
|
||||
EventWaiter& operator=(EventWaiter&&) = delete;
|
||||
|
||||
~EventWaiter()
|
||||
{
|
||||
// Create localExitThreadEvent and localWaitingEvent for capturing. We cannot capture 'this' as we implement move constructor.
|
||||
auto localExitThreadEvent = exitThreadEvent = CreateEvent(nullptr, false, false, nullptr);
|
||||
HANDLE localWaitingEvent = waitingEvent = CreateEvent(nullptr, false, false, name.c_str());
|
||||
std::thread([=]() {
|
||||
HANDLE events[2] = { localWaitingEvent, localExitThreadEvent };
|
||||
while (true)
|
||||
stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts listening for the specified named event. When the event is signaled, the callback is invoked.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the Windows event to listen for.</param>
|
||||
/// <param name="callback">The callback function to invoke when the event is triggered. Receives ERROR_SUCCESS on success.</param>
|
||||
/// <returns>true if listening started successfully, false otherwise.</returns>
|
||||
bool start(const std::wstring& name, std::function<void(DWORD)> callback)
|
||||
{
|
||||
if (m_listening)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_exitThreadEvent = CreateEventW(nullptr, false, false, nullptr);
|
||||
m_waitingEvent = CreateEventW(nullptr, false, false, name.c_str());
|
||||
|
||||
if (!m_exitThreadEvent || !m_waitingEvent)
|
||||
{
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_listening = true;
|
||||
m_eventThread = std::thread([this, cb = std::move(callback)]() {
|
||||
HANDLE events[2] = { m_waitingEvent, m_exitThreadEvent };
|
||||
while (m_listening)
|
||||
{
|
||||
auto waitResult = WaitForMultipleObjects(2, events, false, INFINITE);
|
||||
if (!m_listening)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
// Exit event signaled
|
||||
break;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_FAILED)
|
||||
{
|
||||
callback(GetLastError());
|
||||
cb(GetLastError());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0)
|
||||
{
|
||||
callback(ERROR_SUCCESS);
|
||||
cb(ERROR_SUCCESS);
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
EventWaiter(EventWaiter&) = delete;
|
||||
EventWaiter& operator=(EventWaiter&) = delete;
|
||||
|
||||
EventWaiter(EventWaiter&& a) noexcept
|
||||
/// <summary>
|
||||
/// Stops listening for the event and cleans up resources.
|
||||
/// Waits for the listener thread to finish before returning.
|
||||
/// Safe to call multiple times.
|
||||
/// </summary>
|
||||
void stop()
|
||||
{
|
||||
this->exitThreadEvent = a.exitThreadEvent;
|
||||
this->waitingEvent = a.waitingEvent;
|
||||
|
||||
a.exitThreadEvent = nullptr;
|
||||
a.waitingEvent = nullptr;
|
||||
}
|
||||
|
||||
EventWaiter& operator=(EventWaiter&& a) noexcept
|
||||
{
|
||||
this->exitThreadEvent = a.exitThreadEvent;
|
||||
this->waitingEvent = a.waitingEvent;
|
||||
|
||||
a.exitThreadEvent = nullptr;
|
||||
a.waitingEvent = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
~EventWaiter()
|
||||
{
|
||||
if (exitThreadEvent)
|
||||
m_listening = false;
|
||||
if (m_exitThreadEvent)
|
||||
{
|
||||
SetEvent(exitThreadEvent);
|
||||
CloseHandle(exitThreadEvent);
|
||||
SetEvent(m_exitThreadEvent);
|
||||
}
|
||||
|
||||
if (waitingEvent)
|
||||
if (m_eventThread.joinable())
|
||||
{
|
||||
CloseHandle(waitingEvent);
|
||||
m_eventThread.join();
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the listener is currently active.
|
||||
/// </summary>
|
||||
bool is_listening() const
|
||||
{
|
||||
return m_listening;
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE exitThreadEvent = nullptr;
|
||||
HANDLE waitingEvent = nullptr;
|
||||
void cleanup()
|
||||
{
|
||||
if (m_exitThreadEvent)
|
||||
{
|
||||
CloseHandle(m_exitThreadEvent);
|
||||
m_exitThreadEvent = nullptr;
|
||||
}
|
||||
if (m_waitingEvent)
|
||||
{
|
||||
CloseHandle(m_waitingEvent);
|
||||
m_waitingEvent = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
HANDLE m_exitThreadEvent = nullptr;
|
||||
HANDLE m_waitingEvent = nullptr;
|
||||
std::thread m_eventThread;
|
||||
std::atomic_bool m_listening{ false };
|
||||
};
|
||||
@@ -45,6 +45,7 @@
|
||||
<Target Name="GenerateDscResourceJsonFiles" AfterTargets="Build" Condition="'$(CIBuild)' != 'true'">
|
||||
<Message Text="Generating DSC resource JSON files to DSCModules subfolder..." Importance="high" />
|
||||
<MakeDir Directories="$(TargetDir)DSCModules" />
|
||||
|
||||
<Exec Command="dotnet "$(TargetPath)" manifest --resource settings --outputDir "$(TargetDir)DSCModules"" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -144,7 +144,7 @@ public sealed class AIServiceBatchIntegrationTests
|
||||
switch (format)
|
||||
{
|
||||
case PasteFormats.CustomTextTransformation:
|
||||
var transformResult = await services.CustomActionTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress);
|
||||
var transformResult = await services.CustomActionTransformService.TransformAsync(batchTestInput.Prompt, batchTestInput.Clipboard, null, CancellationToken.None, progress);
|
||||
return DataPackageHelpers.CreateFromText(transformResult.Content ?? string.Empty);
|
||||
|
||||
case PasteFormats.KernelQuery:
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK.WinUI" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
<PackageReference Include="System.ClientModel" />
|
||||
<PackageReference Include="Microsoft.Windows.Compatibility" />
|
||||
|
||||
@@ -225,6 +225,24 @@ internal static class DataPackageHelpers
|
||||
internal static async Task<string> GetHtmlContentAsync(this DataPackageView dataPackageView) =>
|
||||
dataPackageView.Contains(StandardDataFormats.Html) ? await dataPackageView.GetHtmlFormatAsync() : string.Empty;
|
||||
|
||||
internal static async Task<byte[]> GetImageAsPngBytesAsync(this DataPackageView dataPackageView)
|
||||
{
|
||||
var bitmap = await dataPackageView.GetImageContentAsync();
|
||||
if (bitmap == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var pngStream = new InMemoryRandomAccessStream();
|
||||
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, pngStream);
|
||||
encoder.SetSoftwareBitmap(bitmap);
|
||||
await encoder.FlushAsync();
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
await pngStream.AsStreamForRead().CopyToAsync(memoryStream);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
internal static async Task<SoftwareBitmap> GetImageContentAsync(this DataPackageView dataPackageView)
|
||||
{
|
||||
using var stream = await dataPackageView.GetImageStreamAsync();
|
||||
|
||||
@@ -166,5 +166,8 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern uint GetClipboardSequenceNumber();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public enum PasteFormats
|
||||
CanPreview = true,
|
||||
SupportedClipboardFormats = ClipboardFormat.Image,
|
||||
IPCKey = AdvancedPasteAdditionalActions.PropertyNames.ImageToText,
|
||||
KernelFunctionDescription = "Takes an image in the clipboard and extracts all text from it using OCR.")]
|
||||
KernelFunctionDescription = "Takes an image from the clipboard and extracts text using OCR. This function is intended only for explicit text extraction or OCR requests.")]
|
||||
ImageToText,
|
||||
|
||||
[PasteFormatMetadata(
|
||||
@@ -118,8 +118,8 @@ public enum PasteFormats
|
||||
IconGlyph = "\uE945",
|
||||
RequiresAIService = true,
|
||||
CanPreview = true,
|
||||
SupportedClipboardFormats = ClipboardFormat.Text,
|
||||
KernelFunctionDescription = "Takes input instructions and transforms clipboard text (not TXT files) with these input instructions, putting the result back on the clipboard. This uses AI to accomplish the task.",
|
||||
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Image,
|
||||
KernelFunctionDescription = "Takes user instructions and applies them to the current clipboard content (text or image). Use this function for image analysis, description, or transformation tasks beyond simple OCR.",
|
||||
RequiresPrompt = true)]
|
||||
CustomTextTransformation,
|
||||
}
|
||||
|
||||
@@ -40,15 +40,15 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
this.userSettings = userSettings;
|
||||
}
|
||||
|
||||
public async Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var pasteConfig = userSettings?.PasteAIConfiguration;
|
||||
var providerConfig = BuildProviderConfig(pasteConfig);
|
||||
|
||||
return await TransformAsync(prompt, inputText, providerConfig, cancellationToken, progress);
|
||||
return await TransformAsync(prompt, inputText, imageBytes, providerConfig, cancellationToken, progress);
|
||||
}
|
||||
|
||||
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(providerConfig);
|
||||
|
||||
@@ -57,9 +57,9 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputText))
|
||||
if (string.IsNullOrWhiteSpace(inputText) && imageBytes is null)
|
||||
{
|
||||
Logger.LogWarning("Clipboard has no usable text data");
|
||||
Logger.LogWarning("Clipboard has no usable data");
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
Prompt = prompt,
|
||||
InputText = inputText,
|
||||
ImageBytes = imageBytes,
|
||||
ImageMimeType = imageBytes != null ? "image/png" : null,
|
||||
SystemPrompt = systemPrompt,
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface ICustomActionTransformService
|
||||
{
|
||||
Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
|
||||
public string InputText { get; init; }
|
||||
|
||||
public byte[] ImageBytes { get; init; }
|
||||
|
||||
public string ImageMimeType { get; init; }
|
||||
|
||||
public string SystemPrompt { get; init; }
|
||||
|
||||
public AIServiceUsage Usage { get; set; } = AIServiceUsage.None;
|
||||
|
||||
@@ -64,21 +64,13 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
|
||||
var prompt = request.Prompt;
|
||||
var inputText = request.InputText;
|
||||
if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
|
||||
var imageBytes = request.ImageBytes;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(prompt) || (string.IsNullOrWhiteSpace(inputText) && imageBytes is null))
|
||||
{
|
||||
throw new ArgumentException("Prompt and input text must be provided", nameof(request));
|
||||
throw new ArgumentException("Prompt and input content must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
|
||||
var executionSettings = CreateExecutionSettings();
|
||||
var kernel = CreateKernel();
|
||||
var modelId = _config.Model;
|
||||
@@ -102,7 +94,32 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
|
||||
var chatHistory = new ChatHistory();
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
chatHistory.AddUserMessage(userMessageContent);
|
||||
|
||||
if (imageBytes != null)
|
||||
{
|
||||
var collection = new ChatMessageContentItemCollection();
|
||||
if (!string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
collection.Add(new TextContent($"Clipboard Content:\n{inputText}"));
|
||||
}
|
||||
|
||||
collection.Add(new ImageContent(imageBytes, request.ImageMimeType ?? "image/png"));
|
||||
collection.Add(new TextContent($"User instructions:\n{prompt}\n\nOutput:"));
|
||||
chatHistory.AddUserMessage(collection);
|
||||
}
|
||||
else
|
||||
{
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
chatHistory.AddUserMessage(userMessageContent);
|
||||
}
|
||||
|
||||
var response = await chatService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel, cancellationToken);
|
||||
chatHistory.Add(response);
|
||||
|
||||
@@ -67,12 +67,36 @@ public abstract class KernelServiceBase(
|
||||
|
||||
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
|
||||
|
||||
var outputPackage = kernel.GetDataPackage();
|
||||
var hasUsableData = await outputPackage.GetView().HasUsableDataAsync();
|
||||
|
||||
if (kernel.GetLastError() is Exception ex)
|
||||
{
|
||||
throw ex;
|
||||
// If we have an error, but the AI provided a final text response, we can ignore the error (likely a tool failure that the AI handled).
|
||||
// However, if we have usable data (e.g. from a successful tool call before the error?), we might want to keep it?
|
||||
// In the case of ImageToText failure, outputPackage is empty (new DataPackage), hasUsableData is false.
|
||||
// So we check if there is a valid response in the chat history.
|
||||
var lastMessage = chatHistory.LastOrDefault();
|
||||
bool hasAssistantResponse = lastMessage != null && lastMessage.Role == AuthorRole.Assistant && !string.IsNullOrEmpty(lastMessage.Content);
|
||||
|
||||
if (!hasAssistantResponse && !hasUsableData)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// If we have a response or data, we log the error but proceed.
|
||||
Logger.LogWarning($"Kernel operation encountered an error but proceeded with available response/data: {ex.Message}");
|
||||
}
|
||||
|
||||
var outputPackage = kernel.GetDataPackage();
|
||||
if (!hasUsableData)
|
||||
{
|
||||
var lastMessage = chatHistory.LastOrDefault();
|
||||
if (lastMessage != null && lastMessage.Role == AuthorRole.Assistant && !string.IsNullOrEmpty(lastMessage.Content))
|
||||
{
|
||||
outputPackage = DataPackageHelpers.CreateFromText(lastMessage.Content);
|
||||
kernel.SetDataPackage(outputPackage);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await outputPackage.GetView().HasUsableDataAsync()))
|
||||
{
|
||||
@@ -148,7 +172,21 @@ public abstract class KernelServiceBase(
|
||||
var systemPrompt = string.IsNullOrWhiteSpace(runtimeConfig.SystemPrompt) ? DefaultSystemPrompt : runtimeConfig.SystemPrompt;
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
|
||||
var imageBytes = await kernel.GetDataPackageView().GetImageAsPngBytesAsync();
|
||||
if (imageBytes != null)
|
||||
{
|
||||
var collection = new ChatMessageContentItemCollection
|
||||
{
|
||||
new TextContent(prompt),
|
||||
new ImageContent(imageBytes, "image/png"),
|
||||
};
|
||||
chatHistory.AddUserMessage(collection);
|
||||
}
|
||||
else
|
||||
{
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
}
|
||||
|
||||
if (ShouldModerateAdvancedAI())
|
||||
{
|
||||
@@ -302,8 +340,16 @@ public abstract class KernelServiceBase(
|
||||
new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, fixedPrompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
var result = await _customActionTransformService.TransformTextAsync(fixedPrompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
|
||||
var input = await dataPackageView.GetTextOrHtmlTextAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(input) && imageBytes == null)
|
||||
{
|
||||
// If we have no text and no image, try to get text via OCR or throw if nothing exists
|
||||
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
}
|
||||
|
||||
var result = await _customActionTransformService.TransformAsync(fixedPrompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty);
|
||||
});
|
||||
|
||||
@@ -313,15 +359,22 @@ public abstract class KernelServiceBase(
|
||||
new ActionChainItem(format, Arguments: new() { { PromptParameterName, prompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
var imageBytes = await dataPackageView.GetImageAsPngBytesAsync();
|
||||
var input = await dataPackageView.GetTextOrHtmlTextAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(input) && imageBytes == null)
|
||||
{
|
||||
input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
}
|
||||
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, imageBytes, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(output);
|
||||
});
|
||||
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, byte[] imageBytes, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
format switch
|
||||
{
|
||||
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(prompt, input, cancellationToken, progress))?.Content ?? string.Empty,
|
||||
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformAsync(prompt, input, imageBytes, cancellationToken, progress))?.Content ?? string.Empty,
|
||||
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
|
||||
pasteFormat.Format switch
|
||||
{
|
||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformAsync(pasteFormat.Prompt, await clipboardData.GetTextOrHtmlTextAsync(), await clipboardData.GetImageAsPngBytesAsync(), cancellationToken, progress))?.Content ?? string.Empty),
|
||||
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace AdvancedPaste.ViewModels
|
||||
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||
|
||||
private string _currentClipboardHistoryId;
|
||||
private uint _lastClipboardSequenceNumber;
|
||||
private DateTimeOffset? _currentClipboardTimestamp;
|
||||
private ClipboardFormat _lastClipboardFormats = ClipboardFormat.None;
|
||||
private bool _clipboardHistoryUnavailableLogged;
|
||||
@@ -455,6 +456,7 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
ResetClipboardPreview();
|
||||
_currentClipboardHistoryId = null;
|
||||
_lastClipboardSequenceNumber = 0;
|
||||
_currentClipboardTimestamp = null;
|
||||
_lastClipboardFormats = ClipboardFormat.None;
|
||||
return;
|
||||
@@ -477,6 +479,13 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
bool clipboardChanged = formatsChanged;
|
||||
|
||||
var currentSequenceNumber = NativeMethods.GetClipboardSequenceNumber();
|
||||
if (_lastClipboardSequenceNumber != currentSequenceNumber)
|
||||
{
|
||||
clipboardChanged = true;
|
||||
_lastClipboardSequenceNumber = currentSequenceNumber;
|
||||
}
|
||||
|
||||
if (Clipboard.IsHistoryEnabled())
|
||||
{
|
||||
try
|
||||
@@ -652,7 +661,7 @@ namespace AdvancedPaste.ViewModels
|
||||
[RelayCommand]
|
||||
public void OpenSettings()
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste, true);
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste);
|
||||
GetMainWindow()?.Close();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/gpo.h>
|
||||
#include <common/utils/EventWaiter.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
|
||||
@@ -101,6 +103,9 @@ private:
|
||||
bool m_is_advanced_ai_enabled = false;
|
||||
bool m_preview_custom_format_output = true;
|
||||
|
||||
// Event listening for external triggers (e.g., from CmdPal extension)
|
||||
EventWaiter m_triggerEventWaiter;
|
||||
|
||||
Hotkey parse_single_hotkey(const wchar_t* keyName, const winrt::Windows::Data::Json::JsonObject& settingsObject)
|
||||
{
|
||||
try
|
||||
@@ -779,6 +784,17 @@ public:
|
||||
Trace::AdvancedPaste_Enable(true);
|
||||
m_enabled = true;
|
||||
m_process_manager.start();
|
||||
|
||||
// Start listening for external trigger event so we can invoke the same logic as the hotkey.
|
||||
// Note: Use start() directly instead of constructor + move assignment to avoid dangling this pointer in the thread.
|
||||
m_triggerEventWaiter.start(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_EVENT, [this](DWORD) {
|
||||
// Same logic as hotkeyId == 1 (m_advanced_paste_ui_hotkey)
|
||||
Logger::trace(L"AdvancedPaste ShowUI event triggered");
|
||||
m_process_manager.start();
|
||||
m_process_manager.bring_to_front();
|
||||
m_process_manager.send_message(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE);
|
||||
Trace::AdvancedPaste_Invoked(L"AdvancedPasteUIEvent");
|
||||
});
|
||||
};
|
||||
|
||||
void Disable(bool traceEvent)
|
||||
@@ -787,6 +803,9 @@ public:
|
||||
{
|
||||
m_process_manager.stop();
|
||||
|
||||
// Stop event listening
|
||||
m_triggerEventWaiter.stop();
|
||||
|
||||
if (traceEvent)
|
||||
{
|
||||
Trace::AdvancedPaste_Enable(false);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
@@ -11,7 +12,6 @@ using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Microsoft.AdvancedPaste.UITests.Helper;
|
||||
using Microsoft.CodeCoverage.Core.Reports.Coverage;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
@@ -55,7 +55,7 @@ namespace Microsoft.AdvancedPaste.UITests
|
||||
}
|
||||
|
||||
public AdvancedPasteUITest()
|
||||
: base(PowerToysModule.PowerToysSettings, size: WindowSize.Small)
|
||||
: base(PowerToysModule.PowerToysSettings, size: WindowSize.Large_Vertical)
|
||||
{
|
||||
Type currentTestType = typeof(AdvancedPasteUITest);
|
||||
string? dirName = Path.GetDirectoryName(currentTestType.Assembly.Location);
|
||||
@@ -254,13 +254,250 @@ namespace Microsoft.AdvancedPaste.UITests
|
||||
|
||||
/*
|
||||
* Clipboard History
|
||||
- [] Open Settings and Enable clipboard history (if not enabled already). Open Advanced Paste window with hotkey, click Clipboard history and try deleting some entry. Check OS clipboard history (Win+V), and confirm that the same entry no longer exist.
|
||||
- [] Open Advanced Paste window with hotkey, click Clipboard history, and click any entry (but first). Observe that entry is put on top of clipboard history. Check OS clipboard history (Win+V), and confirm that the same entry is on top of the clipboard.
|
||||
- [] Open Settings and Disable clipboard history. Open Advanced Paste window with hotkey and observe that Clipboard history button is disabled.
|
||||
- [x] Open Settings and Enable clipboard history (if not enabled already). Open Advanced Paste window with hotkey, click Clipboard history and try deleting some entry. Check OS clipboard history (Win+V), and confirm that the same entry no longer exist.
|
||||
- [x] Open Advanced Paste window with hotkey, click Clipboard history, and click any entry (but first). Observe that entry is put on top of clipboard history. Check OS clipboard history (Win+V), and confirm that the same entry is on top of the clipboard.
|
||||
- [x] Open Settings and Disable clipboard history. Open Advanced Paste window with hotkey and observe that Clipboard history button is disabled.
|
||||
* Disable Advanced Paste, try different Advanced Paste hotkeys and confirm that it's disabled and nothing happens.
|
||||
*/
|
||||
private void TestCaseClipboardHistory()
|
||||
[TestMethod]
|
||||
[TestCategory("AdvancedPasteUITest")]
|
||||
[TestCategory("TestCaseClipboardHistoryDeleteTest")]
|
||||
public void TestCaseClipboardHistoryDeleteTest()
|
||||
{
|
||||
RestartScopeExe();
|
||||
Thread.Sleep(1500);
|
||||
|
||||
// Find the PowerToys Settings window
|
||||
var settingsWindow = Find<Window>("PowerToys Settings", global: true);
|
||||
Assert.IsNotNull(settingsWindow, "Failed to open PowerToys Settings window");
|
||||
|
||||
if (FindAll<NavigationViewItem>("Advanced Paste").Count == 0)
|
||||
{
|
||||
// Expand Advanced list-group if needed
|
||||
Find<NavigationViewItem>("System Tools").Click();
|
||||
}
|
||||
|
||||
Find<NavigationViewItem>("Advanced Paste").Click();
|
||||
|
||||
Find<ToggleSwitch>("Clipboard history").Toggle(true);
|
||||
|
||||
Session.CloseMainWindow();
|
||||
|
||||
// clear system clipboard
|
||||
ClearSystemClipboardHistory();
|
||||
|
||||
// set test content to clipboard
|
||||
const string textForTesting = "Test text";
|
||||
SetClipboardTextInSTAMode(textForTesting);
|
||||
|
||||
// Open Advanced Paste window with hotkey, click Clipboard history and try deleting some entry.
|
||||
this.SendKeys(Key.Win, Key.Shift, Key.V);
|
||||
Thread.Sleep(1500);
|
||||
|
||||
var apWind = this.Find<Window>("Advanced Paste", global: true);
|
||||
apWind.Find<PowerToys.UITest.Button>("Clipboard history").Click();
|
||||
|
||||
var textGroup = apWind.Find<Group>(textForTesting);
|
||||
Assert.IsNotNull(textGroup, "Cannot find the test string from advanced paste clipboard history.");
|
||||
|
||||
textGroup.Find<PowerToys.UITest.Button>("More options").Click();
|
||||
apWind.Find<TextBlock>("Delete").Click();
|
||||
|
||||
// Check OS clipboard history (Win+V), and confirm that the same entry no longer exist.
|
||||
this.SendKeys(Key.Win, Key.V);
|
||||
|
||||
Thread.Sleep(1500);
|
||||
|
||||
var clipboardWindow = this.Find<Window>("Windows Input Experience", global: true);
|
||||
Assert.IsNotNull(clipboardWindow, "Cannot find system clipboard window.");
|
||||
|
||||
var nothingText = clipboardWindow.Find<Group>("Nothing here, You'll see your clipboard history here once you've copied something.");
|
||||
Assert.IsNotNull(nothingText, "System clipboard is not empty, which should be yes.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("AdvancedPasteUITest")]
|
||||
[TestCategory("TestCaseClipboardHistorySelectTest")]
|
||||
public void TestCaseClipboardHistorySelectTest()
|
||||
{
|
||||
RestartScopeExe();
|
||||
Thread.Sleep(1500);
|
||||
|
||||
// Find the PowerToys Settings window
|
||||
var settingsWindow = Find<Window>("PowerToys Settings", global: true);
|
||||
Assert.IsNotNull(settingsWindow, "Failed to open PowerToys Settings window");
|
||||
|
||||
if (FindAll<NavigationViewItem>("Advanced Paste").Count == 0)
|
||||
{
|
||||
// Expand Advanced list-group if needed
|
||||
Find<NavigationViewItem>("System Tools").Click();
|
||||
}
|
||||
|
||||
Find<NavigationViewItem>("Advanced Paste").Click();
|
||||
|
||||
Find<ToggleSwitch>("Clipboard history").Toggle(true);
|
||||
|
||||
Session.CloseMainWindow();
|
||||
|
||||
// clear system clipboard
|
||||
ClearSystemClipboardHistory();
|
||||
|
||||
// set test content to clipboard
|
||||
string[] textForTesting = { "Test text1", "Test text2", "Test text3", "Test text4", "Test text5", "Test text6", };
|
||||
foreach (var str in textForTesting)
|
||||
{
|
||||
SetClipboardTextInSTAMode(str);
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
|
||||
// Open Advanced Paste window with hotkey
|
||||
this.SendKeys(Key.Win, Key.Shift, Key.V);
|
||||
Thread.Sleep(1500);
|
||||
|
||||
var apWind = this.Find<Window>("Advanced Paste", global: true);
|
||||
apWind.Find<PowerToys.UITest.Button>("Clipboard history").Click();
|
||||
|
||||
// click the 3rd item
|
||||
var textGroup = apWind.Find<Group>(textForTesting[0]);
|
||||
Assert.IsNotNull(textGroup, "Cannot find the test string from advanced paste clipboard history.");
|
||||
textGroup.Click();
|
||||
|
||||
// Check OS clipboard history (Win+V)
|
||||
this.SendKeys(Key.Win, Key.V);
|
||||
|
||||
Thread.Sleep(1500);
|
||||
|
||||
var clipboardWindow = this.Find<Window>("Windows Input Experience", global: true);
|
||||
Assert.IsNotNull(clipboardWindow, "Cannot find system clipboard window.");
|
||||
|
||||
var txtFound = clipboardWindow.Find<Element>(textForTesting[0]);
|
||||
Assert.IsNotNull(txtFound, "Cannot find textblock");
|
||||
}
|
||||
|
||||
// [x] Open Settings and Disable clipboard history.Open Advanced Paste window with hotkey and observe that Clipboard history button is disabled.
|
||||
[TestMethod]
|
||||
[TestCategory("AdvancedPasteUITest")]
|
||||
[TestCategory("TestCaseClipboardHistoryDisableTest")]
|
||||
public void TestCaseClipboardHistoryDisableTest()
|
||||
{
|
||||
RestartScopeExe();
|
||||
Thread.Sleep(1500);
|
||||
|
||||
// Find the PowerToys Settings window
|
||||
var settingsWindow = Find<Window>("PowerToys Settings", global: true);
|
||||
Assert.IsNotNull(settingsWindow, "Failed to open PowerToys Settings window");
|
||||
|
||||
if (FindAll<NavigationViewItem>("Advanced Paste").Count == 0)
|
||||
{
|
||||
// Expand Advanced list-group if needed
|
||||
Find<NavigationViewItem>("System Tools").Click();
|
||||
}
|
||||
|
||||
Find<NavigationViewItem>("Advanced Paste").Click();
|
||||
|
||||
Find<ToggleSwitch>("Clipboard history").Toggle(false);
|
||||
|
||||
Session.CloseMainWindow();
|
||||
|
||||
// set test content to clipboard
|
||||
const string textForTesting = "Test text";
|
||||
SetClipboardTextInSTAMode(textForTesting);
|
||||
|
||||
// Open Advanced Paste window with hotkey, click Clipboard history and try deleting some entry.
|
||||
this.SendKeys(Key.Win, Key.Shift, Key.V);
|
||||
Thread.Sleep(1500);
|
||||
|
||||
var apWind = this.Find<Window>("Advanced Paste", global: true);
|
||||
|
||||
// Click the button (which should still exist but be disabled)
|
||||
apWind.Find<PowerToys.UITest.Button>("Clipboard history").Click();
|
||||
|
||||
// Verify that the clipboard content doesn't appear
|
||||
// Use a short timeout to avoid a long wait when the element doesn't exist
|
||||
Assert.IsFalse(
|
||||
Has<Group>(textForTesting),
|
||||
"Clipboard content should not appear when clipboard history is disabled");
|
||||
}
|
||||
|
||||
// Disable Advanced Paste, try different Advanced Paste hotkeys and confirm that it's disabled and nothing happens.
|
||||
[TestMethod]
|
||||
[TestCategory("AdvancedPasteUITest")]
|
||||
[TestCategory("TestCaseDisableAdvancedPaste")]
|
||||
public void TestCaseDisableAdvancedPaste()
|
||||
{
|
||||
RestartScopeExe();
|
||||
Thread.Sleep(1500);
|
||||
|
||||
// Find the PowerToys Settings window
|
||||
var settingsWindow = Find<Window>("PowerToys Settings", global: true);
|
||||
Assert.IsNotNull(settingsWindow, "Failed to open PowerToys Settings window");
|
||||
|
||||
if (FindAll<NavigationViewItem>("Advanced Paste").Count == 0)
|
||||
{
|
||||
// Expand System Tools if needed
|
||||
Find<NavigationViewItem>("System Tools").Click();
|
||||
}
|
||||
|
||||
Find<NavigationViewItem>("Advanced Paste").Click();
|
||||
|
||||
// Disable Advanced Paste module
|
||||
var moduleToggle = Find<ToggleSwitch>("Enable Advanced Paste");
|
||||
moduleToggle.Toggle(false);
|
||||
|
||||
Session.CloseMainWindow();
|
||||
|
||||
// Prepare some text to test with
|
||||
const string textForTesting = "Test text for disabled module";
|
||||
SetClipboardTextInSTAMode(textForTesting);
|
||||
|
||||
// Try main Advanced Paste hotkey
|
||||
this.SendKeys(Key.Win, Key.Shift, Key.V);
|
||||
Thread.Sleep(500);
|
||||
|
||||
// Verify Advanced Paste window does not appear
|
||||
Assert.IsFalse(
|
||||
Has<Window>("Advanced Paste", global: true),
|
||||
"Advanced Paste window should not appear when the module is disabled");
|
||||
|
||||
// Re-enable Advanced Paste for other tests
|
||||
RestartScopeExe();
|
||||
Thread.Sleep(1500);
|
||||
|
||||
settingsWindow = Find<Window>("PowerToys Settings", global: true);
|
||||
|
||||
if (FindAll<NavigationViewItem>("Advanced Paste").Count == 0)
|
||||
{
|
||||
Find<NavigationViewItem>("System Tools").Click();
|
||||
}
|
||||
|
||||
Find<NavigationViewItem>("Advanced Paste").Click();
|
||||
Find<ToggleSwitch>("Enable Advanced Paste").Toggle(true);
|
||||
|
||||
Session.CloseMainWindow();
|
||||
}
|
||||
|
||||
private void ClearSystemClipboardHistory()
|
||||
{
|
||||
this.SendKeys(Key.Win, Key.V);
|
||||
|
||||
Thread.Sleep(1500);
|
||||
|
||||
var clipboardWindow = this.Find<Window>("Windows Input Experience", global: true);
|
||||
Assert.IsNotNull(clipboardWindow, "Cannot find system clipboard window.");
|
||||
|
||||
clipboardWindow.Find<PowerToys.UITest.Button>("Clear all except pinned items").Click();
|
||||
}
|
||||
|
||||
private void SetClipboardTextInSTAMode(string text)
|
||||
{
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
System.Windows.Forms.Clipboard.SetText(text);
|
||||
});
|
||||
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
}
|
||||
|
||||
private void ContentCopyAndPasteDirectly(string fileName, bool isRTF = false)
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public sealed partial class AnonymousCommand : InvokableCommand
|
||||
{
|
||||
private readonly Action? _action;
|
||||
|
||||
public ICommandResult Result { get; set; } = CommandResult.Dismiss();
|
||||
|
||||
public AnonymousCommand(Action? action)
|
||||
{
|
||||
Name = Properties.Resources.AnonymousCommand_Invoke;
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
if (_action is not null)
|
||||
{
|
||||
_action();
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
// TODO! We probably want to have OnPropertyChanged raise the event
|
||||
// asynchronously, so as to not block the extension app while it's being
|
||||
// processed in the host app.
|
||||
// (also consider this for ItemsChanged in ListPage)
|
||||
public partial class BaseObservable : INotifyPropChanged
|
||||
{
|
||||
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
|
||||
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO #181 - This is dangerous! If the original host goes away,
|
||||
// this can crash as we try to invoke the handlers from that process.
|
||||
// However, just catching it seems to still raise the event on the
|
||||
// new host?
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(propertyName));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public sealed class ChoiceSetSetting : Setting<string>
|
||||
{
|
||||
public partial class Choice
|
||||
{
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
public Choice(string title, string value)
|
||||
{
|
||||
Value = value;
|
||||
Title = title;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Choice> Choices { get; set; }
|
||||
|
||||
private ChoiceSetSetting()
|
||||
: base()
|
||||
{
|
||||
Choices = [];
|
||||
}
|
||||
|
||||
public ChoiceSetSetting(string key, List<Choice> choices)
|
||||
: base(key, choices.ElementAt(0).Value)
|
||||
{
|
||||
Choices = choices;
|
||||
}
|
||||
|
||||
public ChoiceSetSetting(string key, string label, string description, List<Choice> choices)
|
||||
: base(key, label, description, choices.ElementAt(0).Value)
|
||||
{
|
||||
Choices = choices;
|
||||
}
|
||||
|
||||
public override Dictionary<string, object> ToDictionary()
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "type", "Input.ChoiceSet" },
|
||||
{ "title", Label },
|
||||
{ "id", Key },
|
||||
{ "label", Description },
|
||||
{ "choices", Choices },
|
||||
{ "value", Value ?? string.Empty },
|
||||
{ "isRequired", IsRequired },
|
||||
{ "errorMessage", ErrorMessage },
|
||||
};
|
||||
}
|
||||
|
||||
public static ChoiceSetSetting LoadFromJson(JsonObject jsonObject) => new() { Value = jsonObject["value"]?.GetValue<string>() ?? string.Empty };
|
||||
|
||||
public override void Update(JsonObject payload)
|
||||
{
|
||||
// If the key doesn't exist in the payload, don't do anything
|
||||
if (payload[Key] is not null)
|
||||
{
|
||||
Value = payload[Key]?.GetValue<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToState() => $"\"{Key}\": {JsonSerializer.Serialize(Value, JsonSerializationContext.Default.String)}";
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
// shamelessly from https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Management/commands/management/Clipboard.cs
|
||||
public static partial class ClipboardHelper
|
||||
{
|
||||
private static readonly bool? _clipboardSupported = true;
|
||||
|
||||
// Used if an external clipboard is not available, e.g. if xclip is missing.
|
||||
// This is useful for testing in CI as well.
|
||||
private static string? _internalClipboard;
|
||||
|
||||
public static string GetText()
|
||||
{
|
||||
if (_clipboardSupported == false)
|
||||
{
|
||||
return _internalClipboard ?? string.Empty;
|
||||
}
|
||||
|
||||
var tool = string.Empty;
|
||||
var args = string.Empty;
|
||||
var clipboardText = string.Empty;
|
||||
|
||||
ExecuteOnStaThread(() => GetTextImpl(out clipboardText));
|
||||
return clipboardText;
|
||||
}
|
||||
|
||||
public static void SetText(string text)
|
||||
{
|
||||
if (_clipboardSupported == false)
|
||||
{
|
||||
_internalClipboard = text;
|
||||
return;
|
||||
}
|
||||
|
||||
var tool = string.Empty;
|
||||
var args = string.Empty;
|
||||
ExecuteOnStaThread(() => SetClipboardData(Tuple.Create(text, CF_UNICODETEXT)));
|
||||
return;
|
||||
}
|
||||
|
||||
public static void SetRtf(string plainText, string rtfText)
|
||||
{
|
||||
if (s_CF_RTF == 0)
|
||||
{
|
||||
s_CF_RTF = RegisterClipboardFormat("Rich Text Format");
|
||||
}
|
||||
|
||||
ExecuteOnStaThread(() => SetClipboardData(
|
||||
Tuple.Create(plainText, CF_UNICODETEXT),
|
||||
Tuple.Create(rtfText, s_CF_RTF)));
|
||||
}
|
||||
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
private const uint GMEM_MOVEABLE = 0x0002;
|
||||
private const uint GMEM_ZEROINIT = 0x0040;
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
private const uint GHND = GMEM_MOVEABLE | GMEM_ZEROINIT;
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial IntPtr GlobalAlloc(uint flags, UIntPtr dwBytes);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial IntPtr GlobalFree(IntPtr hMem);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
private static partial IntPtr GlobalLock(IntPtr hMem);
|
||||
|
||||
[LibraryImport("kernel32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool GlobalUnlock(IntPtr hMem);
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
|
||||
private static partial void CopyMemory(IntPtr dest, IntPtr src, uint count);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool IsClipboardFormatAvailable(uint format);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool OpenClipboard(IntPtr hWndNewOwner);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool CloseClipboard();
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool EmptyClipboard();
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
private static partial IntPtr GetClipboardData(uint format);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
private static partial IntPtr SetClipboardData(uint format, IntPtr data);
|
||||
|
||||
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial uint RegisterClipboardFormat(string lpszFormat);
|
||||
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
private const uint CF_TEXT = 1;
|
||||
private const uint CF_UNICODETEXT = 13;
|
||||
|
||||
#pragma warning disable SA1308 // Variable names should not be prefixed
|
||||
private static uint s_CF_RTF;
|
||||
#pragma warning restore SA1308 // Variable names should not be prefixed
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
|
||||
private static bool GetTextImpl(out string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsClipboardFormatAvailable(CF_UNICODETEXT))
|
||||
{
|
||||
if (OpenClipboard(IntPtr.Zero))
|
||||
{
|
||||
var data = GetClipboardData(CF_UNICODETEXT);
|
||||
if (data != IntPtr.Zero)
|
||||
{
|
||||
data = GlobalLock(data);
|
||||
text = Marshal.PtrToStringUni(data) ?? string.Empty;
|
||||
GlobalUnlock(data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (IsClipboardFormatAvailable(CF_TEXT))
|
||||
{
|
||||
if (OpenClipboard(IntPtr.Zero))
|
||||
{
|
||||
var data = GetClipboardData(CF_TEXT);
|
||||
if (data != IntPtr.Zero)
|
||||
{
|
||||
data = GlobalLock(data);
|
||||
text = Marshal.PtrToStringAnsi(data) ?? string.Empty;
|
||||
GlobalUnlock(data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore exceptions
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
text = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SetClipboardData(params Tuple<string, uint>[] data)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!OpenClipboard(IntPtr.Zero))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EmptyClipboard();
|
||||
|
||||
foreach (var d in data)
|
||||
{
|
||||
if (!SetSingleClipboardData(d.Item1, d.Item2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool SetSingleClipboardData(string text, uint format)
|
||||
{
|
||||
var hGlobal = IntPtr.Zero;
|
||||
var data = IntPtr.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
uint bytes;
|
||||
if (format == s_CF_RTF || format == CF_TEXT)
|
||||
{
|
||||
bytes = (uint)(text.Length + 1);
|
||||
data = Marshal.StringToHGlobalAnsi(text);
|
||||
}
|
||||
else if (format == CF_UNICODETEXT)
|
||||
{
|
||||
bytes = (uint)((text.Length + 1) * 2);
|
||||
data = Marshal.StringToHGlobalUni(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not yet supported format.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hGlobal = GlobalAlloc(GHND, (UIntPtr)bytes);
|
||||
if (hGlobal == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var dataCopy = GlobalLock(hGlobal);
|
||||
if (dataCopy == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CopyMemory(dataCopy, data, bytes);
|
||||
GlobalUnlock(hGlobal);
|
||||
|
||||
if (SetClipboardData(format, hGlobal) != IntPtr.Zero)
|
||||
{
|
||||
// The clipboard owns this memory now, so don't free it.
|
||||
hGlobal = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore failures
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (data != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(data);
|
||||
}
|
||||
|
||||
if (hGlobal != IntPtr.Zero)
|
||||
{
|
||||
GlobalFree(hGlobal);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ExecuteOnStaThread(Func<bool> action)
|
||||
{
|
||||
const int RetryCount = 5;
|
||||
var tries = 0;
|
||||
|
||||
if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
|
||||
{
|
||||
while (tries++ < RetryCount && !action())
|
||||
{
|
||||
// wait until RetryCount or action
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Exception? exception = null;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (tries++ < RetryCount && !action())
|
||||
{
|
||||
// wait until RetryCount or action
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
exception = e;
|
||||
}
|
||||
});
|
||||
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public sealed class ColorHelpers
|
||||
{
|
||||
public static OptionalColor FromArgb(byte a, byte r, byte g, byte b) => new(true, new(r, g, b, a));
|
||||
|
||||
public static OptionalColor FromRgb(byte r, byte g, byte b) => new(true, new(r, g, b, 255));
|
||||
|
||||
public static OptionalColor Transparent() => new(true, new(0, 0, 0, 0));
|
||||
|
||||
public static OptionalColor NoColor() => new(false, new(0, 0, 0, 0));
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Command : BaseObservable, ICommand
|
||||
{
|
||||
public virtual string Name
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string Id { get; set; } = string.Empty;
|
||||
|
||||
public virtual IconInfo Icon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
= new();
|
||||
|
||||
IIconInfo ICommand.Icon => Icon;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CommandContextItem : CommandItem, ICommandContextItem
|
||||
{
|
||||
public virtual bool IsCritical { get; set; }
|
||||
|
||||
public virtual KeyChord RequestedShortcut { get; set; }
|
||||
|
||||
public CommandContextItem(ICommand command)
|
||||
: base(command)
|
||||
{
|
||||
}
|
||||
|
||||
public CommandContextItem(
|
||||
string title,
|
||||
string subtitle = "",
|
||||
string name = "",
|
||||
Action? action = null,
|
||||
ICommandResult? result = null)
|
||||
{
|
||||
var c = new AnonymousCommand(action);
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
c.Name = name;
|
||||
}
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
c.Result = result;
|
||||
}
|
||||
|
||||
Command = c;
|
||||
|
||||
Title = title;
|
||||
Subtitle = subtitle;
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CommandItem : BaseObservable, ICommandItem
|
||||
{
|
||||
private ICommand? _command;
|
||||
private WeakEventListener<CommandItem, object, IPropChangedEventArgs>? _commandListener;
|
||||
private string _title = string.Empty;
|
||||
|
||||
public virtual IIconInfo? Icon
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Title
|
||||
{
|
||||
get => !string.IsNullOrEmpty(_title) ? _title : _command?.Name ?? string.Empty;
|
||||
|
||||
set
|
||||
{
|
||||
_title = value;
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Subtitle
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Subtitle));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual ICommand? Command
|
||||
{
|
||||
get => _command;
|
||||
set
|
||||
{
|
||||
if (_commandListener is not null)
|
||||
{
|
||||
_commandListener.Detach();
|
||||
_commandListener = null;
|
||||
}
|
||||
|
||||
_command = value;
|
||||
|
||||
if (value is not null)
|
||||
{
|
||||
_commandListener = new(this, OnCommandPropertyChanged, listener => value.PropChanged -= listener.OnEvent);
|
||||
value.PropChanged += _commandListener.OnEvent;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Command));
|
||||
if (string.IsNullOrEmpty(_title))
|
||||
{
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCommandPropertyChanged(CommandItem instance, object source, IPropChangedEventArgs args)
|
||||
{
|
||||
// command's name affects Title only if Title wasn't explicitly set
|
||||
if (args.PropertyName == nameof(ICommand.Name) && string.IsNullOrEmpty(_title))
|
||||
{
|
||||
instance.OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IContextItem[] MoreCommands
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(MoreCommands));
|
||||
}
|
||||
}
|
||||
|
||||
= [];
|
||||
|
||||
public CommandItem()
|
||||
: this(new NoOpCommand())
|
||||
{
|
||||
}
|
||||
|
||||
public CommandItem(ICommand command)
|
||||
{
|
||||
Command = command;
|
||||
}
|
||||
|
||||
public CommandItem(ICommandItem other)
|
||||
{
|
||||
Command = other.Command;
|
||||
Subtitle = other.Subtitle;
|
||||
Icon = (IconInfo?)other.Icon;
|
||||
MoreCommands = other.MoreCommands;
|
||||
}
|
||||
|
||||
public CommandItem(
|
||||
string title,
|
||||
string subtitle = "",
|
||||
string name = "",
|
||||
Action? action = null,
|
||||
ICommandResult? result = null)
|
||||
{
|
||||
var c = new AnonymousCommand(action);
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
c.Name = name;
|
||||
}
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
c.Result = result;
|
||||
}
|
||||
|
||||
Command = c;
|
||||
|
||||
Title = title;
|
||||
Subtitle = subtitle;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2
|
||||
{
|
||||
public virtual string Id { get; protected set; } = string.Empty;
|
||||
|
||||
public virtual string DisplayName { get; protected set; } = string.Empty;
|
||||
|
||||
public virtual IconInfo Icon { get; protected set; } = new IconInfo();
|
||||
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public abstract ICommandItem[] TopLevelCommands();
|
||||
|
||||
public virtual IFallbackCommandItem[]? FallbackCommands() => null;
|
||||
|
||||
public virtual ICommand? GetCommand(string id) => null;
|
||||
|
||||
public virtual ICommandSettings? Settings { get; protected set; }
|
||||
|
||||
public virtual bool Frozen { get; protected set; } = true;
|
||||
|
||||
IIconInfo ICommandProvider.Icon => Icon;
|
||||
|
||||
public virtual void InitializeWithHost(IExtensionHost host) => ExtensionHost.Initialize(host);
|
||||
|
||||
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
|
||||
public virtual void Dispose()
|
||||
{
|
||||
}
|
||||
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
|
||||
|
||||
protected void RaiseItemsChanged(int totalItems = -1)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO #181 - This is the same thing that BaseObservable has to deal with.
|
||||
ItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is used to manually populate the WinRT type cache in CmdPal with
|
||||
/// any interfaces that might not follow a straight linear path of requires.
|
||||
///
|
||||
/// You don't need to call this as an extension author.
|
||||
/// </summary>
|
||||
/// <returns>an array of objects that implement all the leaf interfaces we support</returns>
|
||||
public object[] GetApiExtensionStubs()
|
||||
{
|
||||
return [new SupportCommandsWithProperties()];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A stub class which implements IExtendedAttributesProvider. Just marshalling this
|
||||
/// across the ABI will be enough for CmdPal to store IExtendedAttributesProvider in
|
||||
/// its type cache.
|
||||
/// </summary>
|
||||
private sealed partial class SupportCommandsWithProperties : IExtendedAttributesProvider
|
||||
{
|
||||
public IDictionary<string, object>? GetProperties() => null;
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CommandResult : ICommandResult
|
||||
{
|
||||
public ICommandResultArgs? Args { get; private set; }
|
||||
|
||||
public CommandResultKind Kind { get; private set; } = CommandResultKind.Dismiss;
|
||||
|
||||
public static CommandResult Dismiss()
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.Dismiss,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult GoHome()
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.GoHome,
|
||||
Args = null,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult GoBack()
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.GoBack,
|
||||
Args = null,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult Hide()
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.Hide,
|
||||
Args = null,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult KeepOpen()
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.KeepOpen,
|
||||
Args = null,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult GoToPage(GoToPageArgs args)
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.GoToPage,
|
||||
Args = args,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult ShowToast(ToastArgs args)
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.ShowToast,
|
||||
Args = args,
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult ShowToast(string message)
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.ShowToast,
|
||||
Args = new ToastArgs() { Message = message },
|
||||
};
|
||||
}
|
||||
|
||||
public static CommandResult Confirm(ConfirmationArgs args)
|
||||
{
|
||||
return new CommandResult()
|
||||
{
|
||||
Kind = CommandResultKind.Confirm,
|
||||
Args = args,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Commands;
|
||||
|
||||
public sealed partial class ConfirmableCommand : InvokableCommand
|
||||
{
|
||||
private readonly IInvokableCommand? _command;
|
||||
|
||||
public Func<bool>? IsConfirmationRequired { get; init; }
|
||||
|
||||
public required string ConfirmationTitle { get; init; }
|
||||
|
||||
public required string ConfirmationMessage { get; init; }
|
||||
|
||||
public required IInvokableCommand Command
|
||||
{
|
||||
get => _command!;
|
||||
init
|
||||
{
|
||||
if (_command is INotifyPropChanged oldNotifier)
|
||||
{
|
||||
oldNotifier.PropChanged -= InnerCommand_PropChanged;
|
||||
}
|
||||
|
||||
_command = value;
|
||||
|
||||
if (_command is INotifyPropChanged notifier)
|
||||
{
|
||||
notifier.PropChanged += InnerCommand_PropChanged;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Name));
|
||||
OnPropertyChanged(nameof(Id));
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get => (_command as Command)?.Name ?? base.Name;
|
||||
set
|
||||
{
|
||||
if (_command is Command cmd)
|
||||
{
|
||||
cmd.Name = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Name = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string Id
|
||||
{
|
||||
get => (_command as Command)?.Id ?? base.Id;
|
||||
set
|
||||
{
|
||||
var previous = Id;
|
||||
if (_command is Command cmd)
|
||||
{
|
||||
cmd.Id = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Id = value;
|
||||
}
|
||||
|
||||
if (previous != Id)
|
||||
{
|
||||
OnPropertyChanged(nameof(Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override IconInfo Icon
|
||||
{
|
||||
get => (_command as Command)?.Icon ?? base.Icon;
|
||||
set
|
||||
{
|
||||
if (_command is Command cmd)
|
||||
{
|
||||
cmd.Icon = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Icon = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConfirmableCommand()
|
||||
{
|
||||
// Allow init-only construction
|
||||
}
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public ConfirmableCommand(IInvokableCommand command, string confirmationTitle, string confirmationMessage, Func<bool>? isConfirmationRequired = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(confirmationMessage);
|
||||
ArgumentNullException.ThrowIfNull(confirmationMessage);
|
||||
|
||||
IsConfirmationRequired = isConfirmationRequired;
|
||||
ConfirmationTitle = confirmationTitle;
|
||||
ConfirmationMessage = confirmationMessage;
|
||||
Command = command;
|
||||
}
|
||||
|
||||
private void InnerCommand_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
var property = args.PropertyName;
|
||||
|
||||
if (string.IsNullOrEmpty(property) || property == nameof(Name))
|
||||
{
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(property) || property == nameof(Id))
|
||||
{
|
||||
OnPropertyChanged(nameof(Id));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(property) || property == nameof(Icon))
|
||||
{
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
var showConfirmationDialog = IsConfirmationRequired?.Invoke() ?? true;
|
||||
if (showConfirmationDialog)
|
||||
{
|
||||
return CommandResult.Confirm(new ConfirmationArgs
|
||||
{
|
||||
Title = ConfirmationTitle,
|
||||
Description = ConfirmationMessage,
|
||||
PrimaryCommand = Command,
|
||||
IsPrimaryCommandCritical = true,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return Command.Invoke(this) ?? CommandResult.Dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit.Properties;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CopyPathCommand : InvokableCommand
|
||||
{
|
||||
internal static IconInfo CopyPath { get; } = new("\uE8c8"); // Copy
|
||||
|
||||
private static readonly CompositeFormat CopyFailedFormat = CompositeFormat.Parse(Resources.copy_failed);
|
||||
|
||||
private readonly string _path;
|
||||
|
||||
public CommandResult Result { get; set; } = CommandResult.ShowToast(Resources.CopyPathTextCommand_Result);
|
||||
|
||||
public CopyPathCommand(string fullPath)
|
||||
{
|
||||
this._path = fullPath;
|
||||
this.Name = Resources.CopyPathTextCommand_Name;
|
||||
this.Icon = CopyPath;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipboardHelper.SetText(_path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage("Copy failed: " + ex.Message) { State = MessageState.Error });
|
||||
return CommandResult.ShowToast(
|
||||
new ToastArgs
|
||||
{
|
||||
Message = string.Format(CultureInfo.CurrentCulture, CopyFailedFormat, ex.Message),
|
||||
Result = CommandResult.KeepOpen(),
|
||||
});
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CopyTextCommand : InvokableCommand
|
||||
{
|
||||
public virtual string Text { get; set; }
|
||||
|
||||
public virtual CommandResult Result { get; set; } = CommandResult.ShowToast(Properties.Resources.CopyTextCommand_CopiedToClipboard);
|
||||
|
||||
public CopyTextCommand(string text)
|
||||
{
|
||||
Text = text;
|
||||
Name = Properties.Resources.CopyTextCommand_Copy;
|
||||
Icon = new IconInfo("\uE8C8");
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
ClipboardHelper.SetText(Text);
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class NoOpCommand : InvokableCommand
|
||||
{
|
||||
public override ICommandResult Invoke() => CommandResult.KeepOpen();
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit.Properties;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class OpenFileCommand : InvokableCommand
|
||||
{
|
||||
internal static IconInfo OpenFile { get; } = new("\uE8E5"); // OpenFile
|
||||
|
||||
private readonly string _fullPath;
|
||||
|
||||
public CommandResult Result { get; set; } = CommandResult.Dismiss();
|
||||
|
||||
public OpenFileCommand(string fullPath)
|
||||
{
|
||||
this._fullPath = fullPath;
|
||||
this.Name = Resources.OpenFileCommand_Name;
|
||||
this.Icon = OpenFile;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
using (var process = new Process())
|
||||
{
|
||||
process.StartInfo.FileName = _fullPath;
|
||||
process.StartInfo.UseShellExecute = true;
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Win32Exception ex)
|
||||
{
|
||||
ExtensionHost.LogMessage($"Unable to open {_fullPath}\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit.Properties;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class OpenInConsoleCommand : InvokableCommand
|
||||
{
|
||||
internal static IconInfo OpenInConsoleIcon { get; } = new("\uE756"); // "CommandPrompt"
|
||||
|
||||
private readonly string _path;
|
||||
private bool _isDirectory;
|
||||
|
||||
public OpenInConsoleCommand(string fullPath)
|
||||
{
|
||||
this._path = fullPath;
|
||||
this.Name = Resources.OpenInConsoleCommand_Name;
|
||||
this.Icon = OpenInConsoleIcon;
|
||||
}
|
||||
|
||||
public static OpenInConsoleCommand FromDirectory(string directory) => new(directory) { _isDirectory = true };
|
||||
|
||||
public static OpenInConsoleCommand FromFile(string file) => new(file);
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
using (var process = new Process())
|
||||
{
|
||||
process.StartInfo.WorkingDirectory = _isDirectory ? _path : Path.GetDirectoryName(_path);
|
||||
process.StartInfo.FileName = "cmd.exe";
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Win32Exception ex)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage($"Unable to open '{_path}'\n{ex.Message}\n{ex.StackTrace}") { State = MessageState.Error });
|
||||
}
|
||||
}
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCsWin32;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit.Properties;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class OpenPropertiesCommand : InvokableCommand
|
||||
{
|
||||
internal static IconInfo OpenPropertiesIcon { get; } = new("\uE90F");
|
||||
|
||||
private readonly string _path;
|
||||
|
||||
private static unsafe bool ShowFileProperties(string filename)
|
||||
{
|
||||
var propertiesPtr = Marshal.StringToHGlobalUni("properties");
|
||||
var filenamePtr = Marshal.StringToHGlobalUni(filename);
|
||||
|
||||
try
|
||||
{
|
||||
var info = new Shell32.SHELLEXECUTEINFOW
|
||||
{
|
||||
CbSize = (uint)sizeof(Shell32.SHELLEXECUTEINFOW),
|
||||
LpVerb = propertiesPtr,
|
||||
LpFile = filenamePtr,
|
||||
Show = (int)SHOW_WINDOW_CMD.SW_SHOW,
|
||||
FMask = global::Windows.Win32.PInvoke.SEE_MASK_INVOKEIDLIST,
|
||||
};
|
||||
|
||||
return Shell32.ShellExecuteEx(ref info);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(filenamePtr);
|
||||
Marshal.FreeHGlobal(propertiesPtr);
|
||||
}
|
||||
}
|
||||
|
||||
public OpenPropertiesCommand(string fullPath)
|
||||
{
|
||||
this._path = fullPath;
|
||||
this.Name = Resources.OpenPropertiesCommand_Name;
|
||||
this.Icon = OpenPropertiesIcon;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
try
|
||||
{
|
||||
ShowFileProperties(_path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage($"Error showing file properties '{_path}'\n{ex.Message}\n{ex.StackTrace}") { State = MessageState.Error });
|
||||
}
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class OpenUrlCommand : InvokableCommand
|
||||
{
|
||||
private readonly string _target;
|
||||
|
||||
public CommandResult Result { get; set; } = CommandResult.KeepOpen();
|
||||
|
||||
public OpenUrlCommand(string target)
|
||||
{
|
||||
_target = target;
|
||||
Name = Properties.Resources.OpenUrlCommand_Open;
|
||||
Icon = new IconInfo("\uE8A7");
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
ShellHelpers.OpenInShell(_target);
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCsWin32;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit.Properties;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Commands;
|
||||
|
||||
public partial class OpenWithCommand : InvokableCommand
|
||||
{
|
||||
internal static IconInfo OpenWithIcon { get; } = new("\uE7AC");
|
||||
|
||||
private readonly string _path;
|
||||
|
||||
private static unsafe bool OpenWith(string filename)
|
||||
{
|
||||
var filenamePtr = Marshal.StringToHGlobalUni(filename);
|
||||
var verbPtr = Marshal.StringToHGlobalUni("openas");
|
||||
|
||||
try
|
||||
{
|
||||
var info = new Shell32.SHELLEXECUTEINFOW
|
||||
{
|
||||
CbSize = (uint)sizeof(Shell32.SHELLEXECUTEINFOW),
|
||||
LpVerb = verbPtr,
|
||||
LpFile = filenamePtr,
|
||||
Show = (int)SHOW_WINDOW_CMD.SW_SHOWNORMAL,
|
||||
FMask = global::Windows.Win32.PInvoke.SEE_MASK_INVOKEIDLIST,
|
||||
};
|
||||
|
||||
return Shell32.ShellExecuteEx(ref info);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(filenamePtr);
|
||||
Marshal.FreeHGlobal(verbPtr);
|
||||
}
|
||||
}
|
||||
|
||||
public OpenWithCommand(string fullPath)
|
||||
{
|
||||
this._path = fullPath;
|
||||
this.Name = Resources.OpenWithCommand_Name;
|
||||
this.Icon = OpenWithIcon;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
OpenWith(_path);
|
||||
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ShowFileInFolderCommand : InvokableCommand
|
||||
{
|
||||
private readonly string _path;
|
||||
private static readonly IconInfo Ico = new("\uE838");
|
||||
|
||||
public CommandResult Result { get; set; } = CommandResult.Dismiss();
|
||||
|
||||
public ShowFileInFolderCommand(string path)
|
||||
{
|
||||
_path = path;
|
||||
Name = Properties.Resources.ShowFileInFolderCommand_ShowInFolder;
|
||||
Icon = Ico;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
if (Path.Exists(_path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var argument = "/select, \"" + _path + "\"";
|
||||
Process.Start("explorer.exe", argument);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ConfirmationArgs : IConfirmationArgs
|
||||
{
|
||||
public virtual string? Title { get; set; }
|
||||
|
||||
public virtual string? Description { get; set; }
|
||||
|
||||
public virtual ICommand? PrimaryCommand { get; set; }
|
||||
|
||||
public virtual bool IsPrimaryCommandCritical { get; set; }
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract partial class ContentPage : Page, IContentPage
|
||||
{
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public virtual IDetails? Details
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Details));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IContextItem[] Commands { get; set; } = [];
|
||||
|
||||
public abstract IContent[] GetContent();
|
||||
|
||||
protected void RaiseItemsChanged(int totalItems = -1)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO #181 - This is the same thing that BaseObservable has to deal with.
|
||||
ItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Details : BaseObservable, IDetails
|
||||
{
|
||||
public virtual IIconInfo HeroImage
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(HeroImage));
|
||||
}
|
||||
}
|
||||
|
||||
= new IconInfo();
|
||||
|
||||
public virtual string Title
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string Body
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Body));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual IDetailsElement[] Metadata
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Metadata));
|
||||
}
|
||||
}
|
||||
|
||||
= [];
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class DetailsCommands : IDetailsCommands
|
||||
{
|
||||
public ICommand[]? Commands { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class DetailsElement : IDetailsElement
|
||||
{
|
||||
public virtual string Key { get; set; } = string.Empty;
|
||||
|
||||
public virtual IDetailsData? Data { get; set; }
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class DetailsLink : IDetailsLink
|
||||
{
|
||||
public virtual Uri? Link { get; set; }
|
||||
|
||||
public virtual string Text { get; set; } = string.Empty;
|
||||
|
||||
public DetailsLink()
|
||||
{
|
||||
}
|
||||
|
||||
public DetailsLink(string url)
|
||||
: this(url, url)
|
||||
{
|
||||
}
|
||||
|
||||
public DetailsLink(string url, string text)
|
||||
{
|
||||
if (Uri.TryCreate(url, default(UriCreationOptions), out var newUri))
|
||||
{
|
||||
Link = newUri;
|
||||
}
|
||||
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class DetailsSeparator : IDetailsSeparator
|
||||
{
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract class DynamicListPage : ListPage, IDynamicListPage
|
||||
{
|
||||
public override string SearchText
|
||||
{
|
||||
get => base.SearchText;
|
||||
set
|
||||
{
|
||||
var oldSearch = base.SearchText;
|
||||
SetSearchNoUpdate(value);
|
||||
UpdateSearchText(oldSearch, value);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void UpdateSearchText(string oldSearch, string newSearch);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ExtensionHost
|
||||
{
|
||||
public static IExtensionHost? Host { get; private set; }
|
||||
|
||||
public static void Initialize(IExtensionHost host) => Host = host;
|
||||
|
||||
/// <summary>
|
||||
/// Fire-and-forget a log message to the Command Palette host app. Since
|
||||
/// the host is in another process, we do this in a try/catch in a
|
||||
/// background thread, as to not block the calling thread, nor explode if
|
||||
/// the host app is gone.
|
||||
/// </summary>
|
||||
/// <param name="message">The log message to send</param>
|
||||
public static void LogMessage(ILogMessage message)
|
||||
{
|
||||
if (Host is not null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Host.LogMessage(message);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogMessage(string message)
|
||||
{
|
||||
var logMessage = new LogMessage() { Message = message };
|
||||
LogMessage(logMessage);
|
||||
}
|
||||
|
||||
public static void ShowStatus(IStatusMessage message, StatusContext context)
|
||||
{
|
||||
if (Host is not null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Host.ShowStatus(message, context);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void HideStatus(IStatusMessage message)
|
||||
{
|
||||
if (Host is not null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Host.HideStatus(message);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using WinRT;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions;
|
||||
|
||||
[ComVisible(true)]
|
||||
[GeneratedComClass]
|
||||
internal sealed partial class ExtensionInstanceManager : IClassFactory
|
||||
{
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
|
||||
private const int E_NOINTERFACE = unchecked((int)0x80004002);
|
||||
|
||||
private const int CLASS_E_NOAGGREGATION = unchecked((int)0x80040110);
|
||||
|
||||
private const int E_ACCESSDENIED = unchecked((int)0x80070005);
|
||||
|
||||
// Known constant ignored by win32metadata and cswin32 projections.
|
||||
// https://github.com/microsoft/win32metadata/blob/main/generation/WinSDK/RecompiledIdlHeaders/um/processthreadsapi.h
|
||||
private static readonly HANDLE CURRENT_THREAD_PSEUDO_HANDLE = (HANDLE)(IntPtr)(-6);
|
||||
|
||||
private static readonly Guid IID_IUnknown = Guid.Parse("00000000-0000-0000-C000-000000000046");
|
||||
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
|
||||
private readonly Func<IExtension> _createExtension;
|
||||
|
||||
private readonly bool _restrictToMicrosoftExtensionHosts;
|
||||
|
||||
private readonly Guid _clsid;
|
||||
|
||||
public ExtensionInstanceManager(Func<IExtension> createExtension, bool restrictToMicrosoftExtensionHosts, Guid clsid)
|
||||
{
|
||||
_createExtension = createExtension;
|
||||
_restrictToMicrosoftExtensionHosts = restrictToMicrosoftExtensionHosts;
|
||||
_clsid = clsid;
|
||||
}
|
||||
|
||||
public void CreateInstance(
|
||||
[MarshalAs(UnmanagedType.Interface)] object pUnkOuter,
|
||||
Guid riid,
|
||||
out IntPtr ppvObject)
|
||||
{
|
||||
if (_restrictToMicrosoftExtensionHosts && !IsMicrosoftExtensionHost())
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(E_ACCESSDENIED);
|
||||
}
|
||||
|
||||
ppvObject = IntPtr.Zero;
|
||||
|
||||
if (pUnkOuter is not null)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION);
|
||||
}
|
||||
|
||||
if (riid == _clsid || riid == IID_IUnknown)
|
||||
{
|
||||
// Create the instance of the .NET object
|
||||
var managed = _createExtension();
|
||||
var ins = MarshalInspectable<object>.FromManaged(managed);
|
||||
ppvObject = ins;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The object that ppvObject points to does not support the
|
||||
// interface identified by riid.
|
||||
Marshal.ThrowExceptionForHR(E_NOINTERFACE);
|
||||
}
|
||||
}
|
||||
|
||||
public void LockServer([MarshalAs(UnmanagedType.Bool)] bool fLock)
|
||||
{
|
||||
}
|
||||
|
||||
private unsafe bool IsMicrosoftExtensionHost()
|
||||
{
|
||||
if (PInvoke.CoImpersonateClient() != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint buffer = 0;
|
||||
if (PInvoke.GetPackageFamilyNameFromToken(CURRENT_THREAD_PSEUDO_HANDLE, &buffer, null) != WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var value = new char[buffer];
|
||||
fixed (char* p = value)
|
||||
{
|
||||
if (PInvoke.GetPackageFamilyNameFromToken(CURRENT_THREAD_PSEUDO_HANDLE, &buffer, p) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (PInvoke.CoRevertToSelf() != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var valueStr = new string(value);
|
||||
return valueStr switch
|
||||
{
|
||||
"Microsoft.Windows.CmdPal_8wekyb3d8bbwe\0" or "Microsoft.Windows.CmdPal.Canary_8wekyb3d8bbwe\0" or "Microsoft.Windows.CmdPal.Dev_8wekyb3d8bbwe\0" or "Microsoft.Windows.DevHome_8wekyb3d8bbwe\0" or "Microsoft.Windows.DevHome.Canary_8wekyb3d8bbwe\0" or "Microsoft.Windows.DevHome.Dev_8wekyb3d8bbwe\0" or "Microsoft.WindowsTerminal\0" or "Microsoft.WindowsTerminal_8wekyb3d8bbwe\0" or "WindowsTerminalDev_8wekyb3d8bbwe\0" or "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\0" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/windows/win32/api/unknwn/nn-unknwn-iclassfactory
|
||||
[GeneratedComInterface]
|
||||
[Guid("00000001-0000-0000-C000-000000000046")]
|
||||
internal partial interface IClassFactory
|
||||
{
|
||||
void CreateInstance(
|
||||
[MarshalAs(UnmanagedType.Interface)] object pUnkOuter,
|
||||
Guid riid,
|
||||
out IntPtr ppvObject);
|
||||
|
||||
void LockServer([MarshalAs(UnmanagedType.Bool)] bool fLock);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions;
|
||||
|
||||
public sealed partial class ExtensionServer : IDisposable
|
||||
{
|
||||
private readonly HashSet<int> registrationCookies = [];
|
||||
private ExtensionInstanceManager? _extensionInstanceManager;
|
||||
private ComWrappers? _comWrappers;
|
||||
|
||||
public void RegisterExtension<T>(Func<T> createExtension, bool restrictToMicrosoftExtensionHosts = false)
|
||||
where T : IExtension
|
||||
{
|
||||
Trace.WriteLine($"Registering class object:");
|
||||
Trace.Indent();
|
||||
Trace.WriteLine($"CLSID: {typeof(T).GUID:B}");
|
||||
Trace.WriteLine($"Type: {typeof(T)}");
|
||||
|
||||
int cookie;
|
||||
var clsid = typeof(T).GUID;
|
||||
var wrappedCallback = () => (IExtension)createExtension();
|
||||
_extensionInstanceManager ??= new ExtensionInstanceManager(wrappedCallback, restrictToMicrosoftExtensionHosts, typeof(T).GUID);
|
||||
_comWrappers ??= new StrategyBasedComWrappers();
|
||||
|
||||
var f = _comWrappers.GetOrCreateComInterfaceForObject(_extensionInstanceManager, CreateComInterfaceFlags.None);
|
||||
|
||||
var hr = Ole32.CoRegisterClassObject(
|
||||
ref clsid,
|
||||
f,
|
||||
Ole32.CLSCTX_LOCAL_SERVER,
|
||||
Ole32.REGCLS_MULTIPLEUSE | Ole32.REGCLS_SUSPENDED,
|
||||
out cookie);
|
||||
|
||||
if (hr < 0)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
|
||||
registrationCookies.Add(cookie);
|
||||
Trace.WriteLine($"Cookie: {cookie}");
|
||||
Trace.Unindent();
|
||||
|
||||
hr = Ole32.CoResumeClassObjects();
|
||||
if (hr < 0)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
public void Run() =>
|
||||
|
||||
// TODO : We need to handle lifetime management of the server.
|
||||
// For details around ref counting and locking of out-of-proc COM servers, see
|
||||
// https://docs.microsoft.com/windows/win32/com/out-of-process-server-implementation-helpers
|
||||
Console.ReadLine();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Trace.WriteLine($"Revoking class object registrations:");
|
||||
Trace.Indent();
|
||||
foreach (var cookie in registrationCookies)
|
||||
{
|
||||
Trace.WriteLine($"Cookie: {cookie}");
|
||||
var hr = Ole32.CoRevokeClassObject(cookie);
|
||||
Debug.Assert(hr >= 0, $"CoRevokeClassObject failed ({hr:x}). Cookie: {cookie}");
|
||||
}
|
||||
|
||||
Trace.Unindent();
|
||||
}
|
||||
|
||||
private sealed class Ole32
|
||||
{
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
// https://docs.microsoft.com/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx
|
||||
public const int CLSCTX_LOCAL_SERVER = 0x4;
|
||||
|
||||
// https://docs.microsoft.com/windows/win32/api/combaseapi/ne-combaseapi-regcls
|
||||
public const int REGCLS_MULTIPLEUSE = 1;
|
||||
public const int REGCLS_SUSPENDED = 4;
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
|
||||
// https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-coregisterclassobject
|
||||
[DllImport(nameof(Ole32))]
|
||||
public static extern int CoRegisterClassObject(ref Guid guid, IntPtr obj, int context, int flags, out int register);
|
||||
|
||||
// https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-coresumeclassobjects
|
||||
[DllImport(nameof(Ole32))]
|
||||
public static extern int CoResumeClassObjects();
|
||||
|
||||
// https://docs.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-corevokeclassobject
|
||||
[DllImport(nameof(Ole32))]
|
||||
public static extern int CoRevokeClassObject(int register);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class FallbackCommandItem : CommandItem, IFallbackCommandItem, IFallbackHandler
|
||||
{
|
||||
private readonly IFallbackHandler? _fallbackHandler;
|
||||
|
||||
public FallbackCommandItem(string displayTitle)
|
||||
{
|
||||
DisplayTitle = displayTitle;
|
||||
}
|
||||
|
||||
public FallbackCommandItem(ICommand command, string displayTitle)
|
||||
: base(command)
|
||||
{
|
||||
DisplayTitle = displayTitle;
|
||||
if (command is IFallbackHandler f)
|
||||
{
|
||||
_fallbackHandler = f;
|
||||
}
|
||||
}
|
||||
|
||||
public IFallbackHandler? FallbackHandler
|
||||
{
|
||||
get => _fallbackHandler ?? this;
|
||||
init => _fallbackHandler = value;
|
||||
}
|
||||
|
||||
public virtual string DisplayTitle { get; }
|
||||
|
||||
public virtual void UpdateQuery(string query) => _fallbackHandler?.UpdateQuery(query);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Filter : BaseObservable, IFilter
|
||||
{
|
||||
public virtual IIconInfo Icon
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
= new IconInfo();
|
||||
|
||||
public virtual string Id
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Id));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string Name
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract partial class Filters : BaseObservable, IFilters
|
||||
{
|
||||
public string CurrentFilterId
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(CurrentFilterId));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
// This method should be overridden in derived classes to provide the actual filters.
|
||||
public abstract IFilterItem[] GetFilters();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an icon that is a font glyph.
|
||||
/// This is used for icons that are defined by a specific font face,
|
||||
/// such as Wingdings.
|
||||
///
|
||||
/// Note that Command Palette will default to using the Segoe Fluent Icons,
|
||||
/// Segoe MDL2 Assets font for glyphs in the Segoe UI Symbol range, or Segoe
|
||||
/// UI for any other glyphs. This class is only needed if you want a non-Segoe
|
||||
/// font icon.
|
||||
/// </summary>
|
||||
public partial class FontIconData : IconData, IExtendedAttributesProvider
|
||||
{
|
||||
public string FontFamily { get; set; }
|
||||
|
||||
public FontIconData(string glyph, string fontFamily)
|
||||
: base(glyph)
|
||||
{
|
||||
FontFamily = fontFamily;
|
||||
}
|
||||
|
||||
public IDictionary<string, object>? GetProperties() => new ValueSet()
|
||||
{
|
||||
{ "FontFamily", FontFamily },
|
||||
};
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class FormContent : BaseObservable, IFormContent
|
||||
{
|
||||
public virtual string DataJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(DataJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string StateJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(StateJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string TemplateJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(TemplateJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual ICommandResult SubmitForm(string inputs, string data) => SubmitForm(inputs);
|
||||
|
||||
public virtual ICommandResult SubmitForm(string inputs) => CommandResult.KeepOpen();
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
// Inspired by the fuzzy.rs from edit.exe
|
||||
public static class FuzzyStringMatcher
|
||||
{
|
||||
private const int NOMATCH = 0;
|
||||
|
||||
public static int ScoreFuzzy(string needle, string haystack, bool allowNonContiguousMatches = true)
|
||||
{
|
||||
var (s, _) = ScoreFuzzyWithPositions(needle, haystack, allowNonContiguousMatches);
|
||||
return s;
|
||||
}
|
||||
|
||||
public static (int Score, List<int> Positions) ScoreFuzzyWithPositions(string needle, string haystack, bool allowNonContiguousMatches)
|
||||
{
|
||||
if (string.IsNullOrEmpty(haystack) || string.IsNullOrEmpty(needle))
|
||||
{
|
||||
return (NOMATCH, new List<int>());
|
||||
}
|
||||
|
||||
var target = haystack.ToCharArray();
|
||||
var query = needle.ToCharArray();
|
||||
|
||||
if (target.Length < query.Length)
|
||||
{
|
||||
return (NOMATCH, new List<int>());
|
||||
}
|
||||
|
||||
var targetUpper = FoldCase(haystack);
|
||||
var queryUpper = FoldCase(needle);
|
||||
var targetUpperChars = targetUpper.ToCharArray();
|
||||
var queryUpperChars = queryUpper.ToCharArray();
|
||||
|
||||
var area = query.Length * target.Length;
|
||||
var scores = new int[area];
|
||||
var matches = new int[area];
|
||||
|
||||
for (var qi = 0; qi < query.Length; qi++)
|
||||
{
|
||||
var qiOffset = qi * target.Length;
|
||||
var qiPrevOffset = qi > 0 ? (qi - 1) * target.Length : 0;
|
||||
|
||||
for (var ti = 0; ti < target.Length; ti++)
|
||||
{
|
||||
var currentIndex = qiOffset + ti;
|
||||
var diagIndex = (qi > 0 && ti > 0) ? qiPrevOffset + ti - 1 : 0;
|
||||
var leftScore = ti > 0 ? scores[currentIndex - 1] : 0;
|
||||
var diagScore = (qi > 0 && ti > 0) ? scores[diagIndex] : 0;
|
||||
var matchSeqLen = (qi > 0 && ti > 0) ? matches[diagIndex] : 0;
|
||||
|
||||
var score = (diagScore == 0 && qi != 0) ? 0 :
|
||||
ComputeCharScore(
|
||||
query[qi],
|
||||
queryUpperChars[qi],
|
||||
ti != 0 ? target[ti - 1] : null,
|
||||
target[ti],
|
||||
targetUpperChars[ti],
|
||||
matchSeqLen);
|
||||
|
||||
var isValidScore = score != 0 && diagScore + score >= leftScore &&
|
||||
(allowNonContiguousMatches || qi > 0 ||
|
||||
targetUpperChars.Skip(ti).Take(queryUpperChars.Length).SequenceEqual(queryUpperChars));
|
||||
|
||||
if (isValidScore)
|
||||
{
|
||||
matches[currentIndex] = matchSeqLen + 1;
|
||||
scores[currentIndex] = diagScore + score;
|
||||
}
|
||||
else
|
||||
{
|
||||
matches[currentIndex] = NOMATCH;
|
||||
scores[currentIndex] = leftScore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var positions = new List<int>();
|
||||
if (query.Length > 0 && target.Length > 0)
|
||||
{
|
||||
var qi = query.Length - 1;
|
||||
var ti = target.Length - 1;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var index = (qi * target.Length) + ti;
|
||||
if (matches[index] == NOMATCH)
|
||||
{
|
||||
if (ti == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ti--;
|
||||
}
|
||||
else
|
||||
{
|
||||
positions.Add(ti);
|
||||
if (qi == 0 || ti == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
qi--;
|
||||
ti--;
|
||||
}
|
||||
}
|
||||
|
||||
positions.Reverse();
|
||||
}
|
||||
|
||||
return (scores[area - 1], positions);
|
||||
}
|
||||
|
||||
private static string FoldCase(string input)
|
||||
{
|
||||
return input.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private static int ComputeCharScore(
|
||||
char query,
|
||||
char queryLower,
|
||||
char? targetPrev,
|
||||
char targetCurr,
|
||||
char targetLower,
|
||||
int matchSeqLen)
|
||||
{
|
||||
if (!ConsiderAsEqual(queryLower, targetLower))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var score = 1; // Character match bonus
|
||||
|
||||
if (matchSeqLen > 0)
|
||||
{
|
||||
score += matchSeqLen * 5; // Consecutive match bonus
|
||||
}
|
||||
|
||||
if (query == targetCurr)
|
||||
{
|
||||
score += 1; // Same case bonus
|
||||
}
|
||||
|
||||
if (targetPrev.HasValue)
|
||||
{
|
||||
var sepBonus = ScoreSeparator(targetPrev.Value);
|
||||
if (sepBonus > 0)
|
||||
{
|
||||
score += sepBonus;
|
||||
}
|
||||
else if (char.IsUpper(targetCurr) && matchSeqLen == 0)
|
||||
{
|
||||
score += 2; // CamelCase bonus
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
score += 8; // Start of word bonus
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static bool ConsiderAsEqual(char a, char b)
|
||||
{
|
||||
return a == b || (a == '/' && b == '\\') || (a == '\\' && b == '/');
|
||||
}
|
||||
|
||||
private static int ScoreSeparator(char ch)
|
||||
{
|
||||
return ch switch
|
||||
{
|
||||
'/' or '\\' => 5,
|
||||
'_' or '-' or '.' or ' ' or '\'' or '"' or ':' => 4,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class GalleryGridLayout : BaseObservable, IGalleryGridLayout
|
||||
{
|
||||
public virtual bool ShowTitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowTitle));
|
||||
}
|
||||
}
|
||||
|
||||
= true;
|
||||
|
||||
public virtual bool ShowSubtitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowSubtitle));
|
||||
}
|
||||
}
|
||||
|
||||
= true;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("Usage", "CsWinRT1028:Class is not marked partial", Justification = "Type is not passed across the WinRT ABI", Scope = "type", Target = "~T:WinRT._EventSource_global__Windows_Foundation_TypedEventHandler_object__global__Microsoft_CommandPalette_Extensions_IPropChangedEventArgs_.EventState")]
|
||||
[assembly: SuppressMessage("Usage", "CsWinRT1028:Class is not marked partial", Justification = "Type is not passed across the WinRT ABI", Scope = "type", Target = "~T:WinRT._EventSource_global__Windows_Foundation_TypedEventHandler_object__global__Microsoft_CommandPalette_Extensions_IItemsChangedEventArgs_.EventState")]
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class GoToPageArgs : IGoToPageArgs
|
||||
{
|
||||
public required string PageId { get; set; }
|
||||
|
||||
public NavigationMode NavigationMode { get; set; } = NavigationMode.Push;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
internal interface ISettingsForm
|
||||
{
|
||||
public string ToForm();
|
||||
|
||||
public void Update(JsonObject payload);
|
||||
|
||||
public Dictionary<string, object> ToDictionary();
|
||||
|
||||
public string ToDataIdentifier();
|
||||
|
||||
public string ToState();
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class IconData : IIconData
|
||||
{
|
||||
public IRandomAccessStreamReference? Data { get; set; }
|
||||
|
||||
public string? Icon { get; set; } = string.Empty;
|
||||
|
||||
public IconData(string? icon)
|
||||
{
|
||||
Icon = icon;
|
||||
}
|
||||
|
||||
public IconData(IRandomAccessStreamReference data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
internal IconData()
|
||||
: this(string.Empty)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public sealed class IconHelpers
|
||||
{
|
||||
public static IconInfo FromRelativePath(string path) => new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), path));
|
||||
|
||||
public static IconInfo FromRelativePaths(string lightIcon, string darkIcon) =>
|
||||
new(
|
||||
new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), lightIcon)),
|
||||
new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), darkIcon)));
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class IconInfo : IIconInfo
|
||||
{
|
||||
public virtual IconData Dark { get; set; } = new();
|
||||
|
||||
public virtual IconData Light { get; set; } = new();
|
||||
|
||||
IIconData IIconInfo.Dark => Dark;
|
||||
|
||||
IIconData IIconInfo.Light => Light;
|
||||
|
||||
public IconInfo(string? icon)
|
||||
{
|
||||
Dark = Light = new(icon);
|
||||
}
|
||||
|
||||
public IconInfo(IconData light, IconData dark)
|
||||
{
|
||||
Light = light;
|
||||
Dark = dark;
|
||||
}
|
||||
|
||||
public IconInfo(IconData icon)
|
||||
{
|
||||
Light = icon;
|
||||
Dark = icon;
|
||||
}
|
||||
|
||||
internal IconInfo()
|
||||
: this(string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public static IconInfo FromStream(IRandomAccessStream stream)
|
||||
{
|
||||
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
|
||||
return new IconInfo(data, data);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract partial class InvokableCommand : Command, IInvokableCommand
|
||||
{
|
||||
public virtual ICommandResult Invoke() => CommandResult.KeepOpen();
|
||||
|
||||
public virtual ICommandResult Invoke(object? sender) => Invoke();
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ItemsChangedEventArgs : IItemsChangedEventArgs
|
||||
{
|
||||
public int TotalItems { get; protected set; }
|
||||
|
||||
public ItemsChangedEventArgs(int totalItems = -1)
|
||||
{
|
||||
TotalItems = totalItems;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using static Microsoft.CommandPalette.Extensions.Toolkit.ChoiceSetSetting;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
[JsonSerializable(typeof(float))]
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(Choice))]
|
||||
[JsonSerializable(typeof(List<Choice>))]
|
||||
[JsonSerializable(typeof(List<ChoiceSetSetting>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
|
||||
[JsonSerializable(typeof(List<Dictionary<string, object>>))]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true)]
|
||||
internal sealed partial class JsonSerializationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user