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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ SDK 的源码都在本仓库的 `sdk/` 下,发布在[本仓库的 Releases](ht
| [原生构建工作流(开发中)](guides/native-build-workflows.md) | Node/Java SDK 的构建执行、精确测试、运行准备,以及未提交源码的真实 Core 验证 |
| [构建、运行与测试接入](guides/build-tool-integration.md) | 可用的执行提供者示例、工程后端边界,以及开发中的 API 3 状态 |
| [编写调试适配器插件](guides/debug-adapter-plugin.md) | API 4:从最小清单到启动方式、先编译再调试的 `executable`、调试控制台补全,以及预发布与转正 |
| [插件后端:工作区上下文与宿主服务(开发中)](guides/plugin-backend-workspace.md) | `backend` 2 与协议 3:免页面启动、工作区上下文、宿主通知、失败码,以及协议 3 只增不改时后端要遵守的规则 |
| [插件后端:工作区上下文与宿主服务(开发中)](guides/plugin-backend-workspace.md) | `backend` 2 与协议 3:免页面启动、工作区上下文、纯后端命令(`ui` 4)、文档/文件/窗口宿主方法、宿主通知、失败码,以及协议 3 只增不改时后端要遵守的规则 |

收录和上架的规则由 Lithe Store 维护:[发布社区插件](https://github.com/Lithe-IDEA/Lithe-store/blob/main/docs/publishing.md)讲怎么打包、提交收录和发布新版本,[版本追踪与更新](https://github.com/Lithe-IDEA/Lithe-store/blob/main/docs/versioning-and-updates.md)讲 Lithe 怎么选择可安装的版本。

Expand Down
12 changes: 8 additions & 4 deletions contract.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"source": {
"repository": "https://github.com/1lck/Lithe-IDEA",
"directory": "shared/contracts/plugins/v1",
"revision": "f20601ee6194f32d597031556ea628de586ac2e2"
"revision": "9175bd73431aec5086f1fe4c532be54fc49ba4e8"
},
"files": [
{
"path": "README.md",
"sha256": "1ac89012a6e620ddeec63b0fc82460c56f05623a1d2a29dcf45e87fa123a4806"
"sha256": "b2864095465511324cfde17968184fb87ac9191711c18a68fd48c5db91149f35"
},
{
"path": "fixtures/backend-workspace.plugin.json",
Expand Down Expand Up @@ -94,6 +94,10 @@
"path": "fixtures/build-tool-path.plugin.json",
"sha256": "685144e417bbf01b013144dec109d51b56922f98811e9ea6eda34da26749165c"
},
{
"path": "fixtures/command-backend.plugin.json",
"sha256": "791f11f3b9d59d5909a9ca94788bddb7f7493b7610361553d5733d7aed18df22"
},
{
"path": "fixtures/command-plans.json",
"sha256": "a188fd65f6cdb47f9e9ad4b0de3a7ddcd17e4e577b34f6e25b96d2c1f2bdca7c"
Expand All @@ -116,7 +120,7 @@
},
{
"path": "fixtures/function-protocol.json",
"sha256": "d8f11ab3287d24210e9369ec6b5fcdb65316d6afe82c60583455c84065ae583b"
"sha256": "c00b99c31f4c2b5d2cb7644d123cdec69d8bc439a5f043e5b655db6351545cff"
},
{
"path": "fixtures/hello-lithe.plugin.json",
Expand Down Expand Up @@ -228,7 +232,7 @@
},
{
"path": "plugin-manifest.schema.json",
"sha256": "789a340f03a598f9e1505ff887a90ee5a0b8a004071ef331d6382a97ea1b9f11"
"sha256": "3065b75d2a2e3913e6ac002acfe50f9da179e703603b06753c9bfd171ffe8905"
}
]
}
103 changes: 100 additions & 3 deletions contract/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ hosts. Level 3 versions each section on its own. A level 3 manifest lists in
| `serverProcess` | The launch fields of language servers, of build providers with a process of their own, of VCS providers, and of the plugin backend: `launch`, `arguments`, `validationArguments`, `runtimeDependencies`, `initializationOptions` | `1` |
| `executionProviders` | `executionProviders[]` | `1` |
| `settings` | `settings[]` and `settingActions[]` | `1`, `2` (adds [placement, scope, effect, page buttons](#settings-v2-placement-scope-effect-and-page-buttons)) |
| `ui` | `ui` | `1`, `2` (adds [web content](#web-content-ui-2)) |
| `ui` | `ui` | `1`, `2` (web content), `3` (native controls), `4` ([backend-only commands](#backend-only-commands-ui-4)) |
| `buildSystems` | Declarative build detection, models, and command plans | `1` |
| `buildProviders` | `buildSystems[].provider`, including own-process and shared language-server providers; v2 tasks, v3 tests, v4 run, v5 native debug preparation, v6 read-only model inputs | `1`, `2`, `3`, `4`, `5`, `6` |
| `buildModelInputs` | Optional public model sharing between enabled workspace plugins | `1` |
Expand Down Expand Up @@ -1876,7 +1876,7 @@ granted services as `lsp.startServer.hostServices` (from the launch plan's
- A session holds at most 16 unanswered host requests; a further one is
refused with `plugin.host.failed`. Unanswered requests end with the session.
- `hostServices` needs `pluginId` and names only services this Core defines
(`credentials`, `workspace`); otherwise `lsp.startServer` is
(`credentials`, `workspace`, or registered method-grant names); otherwise `lsp.startServer` is
`invalid_request`.

### Workspace contexts and capability discovery (protocol version 3)
Expand All @@ -1902,7 +1902,7 @@ notifications.
grants only the services it has handlers for, so a listed method never
fails for want of one. `backendAuthority` is `"local"`: the backend runs on
the machine that runs Lithe. `limits` gives `pendingHostRequests`,
`functionResultBytes`, `errorMessageBytes` (the cut of a host failure's
`functionResultBytes`, `hostRequestBytes`, `hostResultBytes`, `errorMessageBytes` (the cut of a host failure's
message), `errorDetailsBytes`, and `notificationBytes`. A backend must
check `services` instead of assuming a method from the version.
- A context is one open workspace in one window:
Expand Down Expand Up @@ -2071,6 +2071,103 @@ So that a version 3 backend works with every host of version 3:
`fixtures/function-protocol.json` (`hostRequests`) holds the
unsupported-option cases.

### Public document, file and window methods (protocol version 3)

These methods are implemented by the macOS host. Windows does not advertise
`backend` support. `getCapabilities` lists only the intersection of the plugin's
permission-derived grants and this host's registered method handlers.

New grants in `lsp.startServer.hostServices` use **full method names**, not service
names. The legacy `workspace` grant still admits only `listContexts`, `getContext`
and `getFolders`; it never grants file reads. The legacy `credentials` grant is
unchanged. Registering another handler cannot replace a method already registered.

All methods below require `context: {contextID, generation}`. Resource arguments
use `{rootID, path}` from that plugin's `getFolders` response; paths use `/` and
are relative to the root. A root ID is not a lifetime or an authorization token.
Opening the same folder in two windows creates independent context-owned leases;
closing one does not invalidate the other, and reopening works with generation 1
in its new context. Remote contexts are explicitly refused by this first set of
methods. Unknown options fail before side effects with `unsupportedOption`.

| Methods (prefix `lithe/host/`) | Permission | Params in addition to `context` | Result |
| --- | --- | --- | --- |
| `documents/list` | `documents.read` | none | `{documents:[<summary>]}` |
| `documents/getSnapshot` | `documents.read` | `documentID` | `{document:<summary>, text}` from the dirty live buffer |
| `editor/getActive` | `documents.read` | none | summary of the foreground text tab, or `null` |
| `editor/getSelections` | `documents.read` | `documentID` | `{documentID,version,selections:[{anchor,active}],primaryIndex}` |
| `editor/applyEdits` | `editor.edit` | `documentID,expectedVersion,edits:[{range,text}]` | `{documentID,version,applied}` |
| `editor/replaceSelections` | `editor.edit` | `documentID,expectedVersion,selections:[{anchor,active}],replacements:[string]` | same; one replacement for all selections or one per selection |
| `editor/insertText` | `editor.edit` | `documentID,expectedVersion,at,text` | same |
| `workspace/readDirectory` | `workspace.files.read` | `resource,cursor?,limit?` | `{entries:[{name,resource,kind}],nextCursor}` |
| `workspace/stat` | `workspace.files.read` | `resource` | `{resource,kind,size,modifiedMilliseconds}` |
| `workspace/readFile` | `workspace.files.read` | `resource,maximumBytes?` | `{resource,source:"disk",byteCount,isTruncated,identity,text?,encoding?,base64?}` |
| `workspace/watch` | `workspace.files.watch` | `resource,recursive?` (default false) | `{subscriptionID}`; events contain `{rootID,changedPaths,requiresRescan}` |
| `workspace/findFiles` | `workspace.search` | `resource` naming the root (`path:""`), `query,fileMask?,cursor?,limit?` | `{matches:[<resource>],nextCursor,isTruncated,source:"disk"}` |
| `workspace/findText` | `workspace.search` | root `resource,pattern,fileMask?,caseSensitive?,wholeWords?,regularExpression?,cursor?,limit?` | same envelope, matches `{resource,line,preview,range:null}`; the upstream search owner currently reports lines, not exact match spans |
| `editor/open`, `editor/reveal` | `editor.open` | `resource,activation?` (`select` or `background`), `range?,tab?` (`regular` only) | document identity/version and state, `wasAlreadyOpen,windowIsForeground,appliedSelection` |
| `preview/openHTML` | `preview.open` | HTML `resource,presentation?` (`editorAndPreview` or `previewOnly`), `range?` | open result plus `usesLiveBuffer:true,presentation`; explicitly changes the existing tab's presentation |
| `browser/openExternal` | `browser.open` | exactly one of HTTP(S) `url` or HTML `resource`; `unsaved?` (`openSavedFile` or `requireSaved`) for a resource | `{opened:true,source:"web"|"disk",hasUnsavedChanges?}` |
| `window/showInformation`, `window/showWarning`, `window/showError` | `window.messages` | `text,actions?` | `{selectedIndex}`; zero-based, or `null` on dismissal |

A document summary has `documentID,version,path,languageID,isReadOnly,isDirty,
encoding,lineEnding`. `path` is workspace-relative or null for a document outside
the root; `languageID` may be null. Positions `{line,column}` are one-based UTF-16;
ranges `{start,end}` are half-open and cannot split a surrogate pair. Selections
keep anchor/active direction. Batches are atomic and one Monaco undo step. Stale
versions, read-only buffers, pending editor synchronization and IME composition
are refused. Background documents without a mounted editor can be listed/read,
but edits/selections may return `busy`; opening a foreground tab does not imply
that its web editor is already ready. Retry reads after readiness, not edits
blindly. `editor.open/reveal` currently applies a range's start as a caret and
reports `{kind:"caret",line,column}`, not a fabricated full selection.

Limits: snapshots and file reads expose at most 128 KiB of source text/bytes per
call; oversized snapshots return `limitExceeded`, file reads report truncation.
This leaves room for worst-case JSON escaping under the 1 MiB serialized host
request/result budget. Batches contain at most 1,000 edits/selections. Directory
pages default to 200 and cap at 1,000; search pages default to 100 and cap at 200,
with the search owner's overall 200-result cap. Explicit file reads use
component-wise `O_NOFOLLOW`; listings omit symlinks. Hidden-file rules affect
listing/search/watch, not authorization for explicit reads. Open/browser paths
retain the existing canonical containment checks and their documented
check-to-open race; native backends are not OS-sandboxed.

Machine host requests have a 30-second host deadline. User-choice messages wait
until the user answers, the request is cancelled, or the context/session closes.
Messages retain the per-plugin/window rate and queue limits (5/10 seconds,
3 pending per plugin, 10 per window, 2,000 text characters, 3 actions of 40
characters). Session exit revokes grants and cancels requests/subscriptions;
old session callbacks cannot affect a replacement. Closing a context cancels its
requests with `contextExpired`. Notifications have a 128-entry per-session queue;
context revisions coalesce to the latest, and overflow ends the session instead
of allowing unbounded memory or silently losing commands. Notification/response
writes share the process write watchdog and stop cleanup with function requests.

Cancellation or timeout **does not promise rollback**: an edit or external open
may already have committed. The SDK must not replay a mutation on these failures.
On `versionConflict`, reread and recompute; on `contextExpired`, resolve the
original target again, never substitute the focused window. `busy`/`rateLimited`
may be retried with bounds; `permissionDenied`/`readOnly` need user intervention.

### Backend-only commands (`ui` 4)

`requires.ui:4` permits `ui.commands` without `toolWindowID` and allows an empty
or omitted `toolWindows` array. Such a command requires `backend:2`, the existing
`ui.contribute` and `backend.launch` permissions, and its `onCommand:<id>` event.
Versions 1-3 retain their window-required behavior. An old host rejects ui 4 at
installation, rather than accepting a command it cannot execute. See
`fixtures/command-backend.plugin.json`.

The command appears through the ordinary workbench command contributions and
sends `commands/didRun` without opening a dummy tool window. Commands arriving
while initialize is pending wait until ready; a crashed backend can start again
on its declared command event. Background workspace activation has two concurrent
start slots; explicit command activation does not wait for unrelated background
plugins. `didRun` remains an event, not an acknowledgement that the command's
business action succeeded; a future result-bearing execute API must use the
existing request lifecycle rather than infer success from notification delivery.

### Compatibility

- Packages of every API level are unchanged: functions add no manifest field.
Expand Down
50 changes: 50 additions & 0 deletions contract/v1/fixtures/command-backend.plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"schemaVersion": 2,
"id": "dev.lithe.fixture.workspacebackend",
"displayName": "Workspace Backend",
"version": "1.0.0",
"pluginAPIVersion": 3,
"requires": {
"backend": 2,
"serverProcess": 1,
"ui": 4
},
"vendor": {
"id": "org.example",
"displayName": "Example",
"signatureRequirement": "publisherPackage"
},
"permissions": [
"backend.launch",
"workspace.metadata",
"ui.contribute",
"documents.read",
"editor.edit"
],
"activationEvents": [
"onCommand:dev.lithe.fixture.workspacebackend.transform"
],
"languages": [],
"backend": {
"launch": {
"kind": "packaged",
"path": "bin/backend"
},
"arguments": [
"--state",
"${stateDir}"
]
},
"ui": {
"toolWindows": [],
"commands": [
{
"id": "dev.lithe.fixture.workspacebackend.transform",
"title": "Transform selection",
"placements": [
"commandPalette"
]
}
]
}
}
5 changes: 5 additions & 0 deletions contract/v1/fixtures/function-protocol.json
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@
}
],
"hostRequests": [
{"name": "workspace metadata never authorizes file reads", "version": 3, "hostServices": ["workspace"], "method": "lithe/host/workspace/readFile", "params": {"context": {"contextID": "w1", "generation": 1}}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}},
{"name": "editor read method grant never authorizes edits", "version": 3, "hostServices": ["lithe/host/editor/getSelections"], "method": "lithe/host/editor/applyEdits", "params": {}, "expected": {"rpcCode": -32010, "code": "plugin.host.permissionDenied"}},
{"name": "only the explicit file method is forwarded", "version": 3, "hostServices": ["lithe/host/workspace/readFile"], "method": "lithe/host/workspace/readFile", "params": {"context": {"contextID": "w1", "generation": 1}, "resource": {"rootID": "r1", "path": "hello.txt"}}, "expected": {"forward": "workspace"}},
{
"name": "version 1 sessions get no host service even when granted",
"version": 1,
Expand Down Expand Up @@ -410,6 +413,8 @@
"limits": {
"pendingHostRequests": 16,
"functionResultBytes": 1048576,
"hostRequestBytes": 1048576,
"hostResultBytes": 1048576,
"errorMessageBytes": 1024,
"errorDetailsBytes": 4096,
"notificationBytes": 1048576
Expand Down
20 changes: 13 additions & 7 deletions contract/v1/plugin-manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"projectGenerators": { "description": "Entries of the New Project window: fields, requirements, and steps.", "enum": [1] },
"serverProcess": { "description": "The launch fields of language servers, of build providers that start their own process, of VCS providers, of debug adapters, and of the plugin backend. Version 2 adds runSelectedRuntime and is required exactly when a debug adapter uses it (pluginAPIVersion 4).", "enum": [1, 2] },
"settings": { "enum": [1] },
"ui": { "description": "Version 2 adds web content; version 3 adds native controls.", "enum": [1, 2, 3] },
"ui": { "description": "Version 2 adds web content; version 3 adds native controls; version 4 adds backend-only commands.", "enum": [1, 2, 3, 4] },
"vcsProviders": { "description": "Required exactly when vcsProviders is non-empty. The provider protocol itself is negotiated in initialize (shared/contracts/vcs-provider-v1.md).", "enum": [1] }
}
},
Expand All @@ -60,7 +60,16 @@
},
"permissions": {
"type": "array",
"items": { "enum": ["backend.launch", "credentials.store", "debug.attach", "debug.launch", "languageServer.launch", "process.build", "process.run", "process.test", "process.vcs", "project.generate", "ui.contribute", "ui.web", "workspace.metadata"] }
"items": { "enum": ["backend.launch", "credentials.store", "debug.attach", "debug.launch", "languageServer.launch", "process.build", "process.run", "process.test", "process.vcs", "project.generate", "ui.contribute", "ui.web", "workspace.metadata",
"workspace.files.read",
"workspace.files.watch",
"workspace.search",
"documents.read",
"editor.edit",
"editor.open",
"preview.open",
"browser.open",
"window.messages"] }
},
"activationEvents": {
"type": "array",
Expand Down Expand Up @@ -114,13 +123,11 @@
"ui": {
"type": "object",
"additionalProperties": false,
"required": [
"toolWindows"
],
"description": "ui 4 permits backend commands without toolWindowID or tool windows; older ui versions require a tool window.",
"properties": {
"toolWindows": {
"type": "array",
"minItems": 1,
"minItems": 0,
"maxItems": 64,
"items": {
"type": "object",
Expand Down Expand Up @@ -189,7 +196,6 @@
"required": [
"id",
"title",
"toolWindowID",
"placements"
],
"properties": {
Expand Down
Loading
Loading