Skip to content
This repository has been archived by the owner on May 7, 2024. It is now read-only.

Commit

Permalink
Support pausing text checking session (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
znck authored May 12, 2022
1 parent 7c4a374 commit 75fce63
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 26 deletions.
14 changes: 14 additions & 0 deletions .changeset/strong-beans-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'grammarly': patch
'grammarly-languageclient': patch
'grammarly-languageserver': patch
---

Pause text checking session

- Commands:
- `Grammarly: Pause text check` — Available when active editor has an active Grammarly session
- `Grammarly: Resume text check` — Available when active editor has a paused Grammarly session
- `Grammarly: Restart language server`
- Configuration:
- `grammarly.startTextCheckInPausedState` — When enabled, new text checking session is paused initially
7 changes: 4 additions & 3 deletions extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ This extension brings [Grammarly](https://grammarly.com) to VS Code.

The status of the Grammarly text-checking session is displayed on the status bar (bottom right).

| Connecting | Checking | Done | Error |
| ----------------------------------- | --------------------------------- | ----------------------------- | ------------------------------ |
| ![](./assets/status-connecting.png) | ![](./assets/status-checking.png) | ![](./assets/status-done.png) | ![](./assets/status-error.png) |
| Session | Connecting | Checking | Done | Paused | Error |
| ----------------- | ----------------------------------- | --------------------------------- | ---------------------------------- | ------------------------------- | ------------------------------ |
| Anonymous | ![](./assets/status-connecting.png) | ![](./assets/status-checking.png) | ![](./assets/status-done.png) | ![](./assets/status-paused.png) | ![](./assets/status-error.png) |
| Grammarly Account | ![](./assets/status-connecting.png) | ![](./assets/status-checking.png) | ![](./assets/status-connected.png) | ![](./assets/status-paused.png) | ![](./assets/status-error.png) |

## How to get help

Expand Down
Binary file added extension/assets/status-connected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extension/assets/status-paused.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 31 additions & 3 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
"scope": "window",
"order": 1
},
"grammarly.startTextCheckInPausedState": {
"type": "boolean",
"description": "Start text checking session in paused state",
"default": false
},
"grammarly.config.documentDialect": {
"markdownDescription": "Specific variety of English being written. See [this article](https://support.grammarly.com/hc/en-us/articles/115000089992-Select-between-British-English-American-English-Canadian-English-and-Australian-English) for differences.",
"enum": [
Expand Down Expand Up @@ -304,19 +309,42 @@
"title": "Check text",
"category": "Grammarly",
"command": "grammarly.check",
"icon": "$(pass-filled)"
"icon": "$(pass-filled)",
"enablement": "!grammarly.isActive"
},
{
"title": "Log in / Connect your account",
"category": "Grammarly",
"command": "grammarly.login",
"icon": "$(log-in)"
"icon": "$(log-in)",
"enablement": "!grammarly.isRunning || !grammarly.isUserAccountConnected"
},
{
"title": "Log out",
"category": "Grammarly",
"command": "grammarly.logout",
"icon": "$(log-out)"
"icon": "$(log-out)",
"enablement": "grammarly.isUserAccountConnected"
},
{
"title": "Restart language server",
"category": "Grammarly",
"command": "grammarly.restartServer",
"icon": "$(debug-restart)"
},
{
"title": "Pause text check",
"category": "Grammarly",
"command": "grammarly.pauseCheck",
"icon": "$(debug-pause)",
"enablement": "grammarly.isActive && !grammarly.isPaused"
},
{
"title": "Resume text check",
"category": "Grammarly",
"command": "grammarly.resumeCheck",
"icon": "$(debug-start)",
"enablement": "grammarly.isActive && grammarly.isPaused"
}
]
},
Expand Down
6 changes: 5 additions & 1 deletion extension/src/GrammarlyClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ export class GrammarlyClient implements Registerable {
selector.language != null || selector.pattern != null || selector.scheme != null ? (selector as any) : null,
)
.filter(<T>(value: T | null): value is T => value != null),

initializationOptions: {
startTextCheckInPausedState: config.get<boolean>('startTextCheckInPausedState'),
},
revealOutputChannelOn: 3,
progressOnInitialization: true,
errorHandler: {
Expand Down Expand Up @@ -179,6 +181,7 @@ export class GrammarlyClient implements Registerable {
this.client = this.createClient()
this.session = this.client.start()
await this.client.onReady()
await commands.executeCommand('setContext', 'grammarly.isRunning', true)
this.isReady = true
this.callbacks.forEach((fn) => {
try {
Expand All @@ -188,6 +191,7 @@ export class GrammarlyClient implements Registerable {
}
})
} catch (error) {
await commands.executeCommand('setContext', 'grammarly.isRunning', false)
await window.showErrorMessage(`The extension couldn't be started. See the output channel for details.`)
} finally {
statusbar.dispose()
Expand Down
42 changes: 35 additions & 7 deletions extension/src/StatusBarController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { commands, Disposable, StatusBarAlignment, TextDocument, window, workspace } from 'vscode'
import { commands, Disposable, StatusBarAlignment, TextDocument, Uri, window, workspace } from 'vscode'
import { GrammarlyClient } from './GrammarlyClient'

type Status = 'idle' | 'connecting' | 'checking' | 'error'
type Status = 'idle' | 'connecting' | 'checking' | 'error' | 'paused'

export class StatusBarController {
#current: { uri: string; status: Status | null } | null = null
Expand Down Expand Up @@ -36,6 +36,18 @@ export class StatusBarController {
isRestarting = false
}
}),
commands.registerCommand('grammarly.pauseCheck', async (uri?: Uri) => {
const id = uri ?? window.activeTextEditor?.document.uri
if (id == null) return
await this.grammarly.client.protocol.pause(id.toString())
await this.update()
}),
commands.registerCommand('grammarly.resumeCheck', async (uri?: Uri) => {
const id = uri ?? window.activeTextEditor?.document.uri
if (id == null) return
await this.grammarly.client.protocol.resume(id.toString())
await this.update()
}),
)
}

Expand All @@ -47,11 +59,12 @@ export class StatusBarController {
public async update(): Promise<void> {
await Promise.resolve()
const document = window.activeTextEditor?.document
if (document == null) return this.#statusbar.hide()
const isUser = await this.grammarly.client.protocol.isUserAccountConnected()
await commands.executeCommand('setContext', 'grammarly.isUserAccountConnected', isUser)
if (document == null) return this.hide()
const status = await this.getStatus(document)
this.#current = { uri: document.uri.toString(), status }
if (status == null && !this.grammarly.matchesDocumentSelector(document)) return this.#statusbar.hide()
const isUser = await this.grammarly.client.protocol.isUserAccountConnected()
if (status == null && !this.grammarly.matchesDocumentSelector(document)) return this.hide()
const accountIcon = isUser ? '$(account)' : ''
const statusIcon =
status == null
Expand All @@ -62,22 +75,37 @@ export class StatusBarController {
? accountIcon + '$(warning)'
: status === 'idle'
? accountIcon + '$(pass-filled)'
: status === 'paused'
? '$(debug-start)'
: '$(loading~spin)'
this.#statusbar.text = statusIcon
this.#statusbar.color = status === 'error' ? 'red' : ''
this.#statusbar.accessibilityInformation = {
label: status ?? '',
role: status === 'error' ? 'button' : undefined,
role: 'button',
}

this.#statusbar.tooltip = [
`Your Grammarly account is ${isUser ? '' : 'not '}used for this file.`,
`Connection status: ${status}`,
status === 'error' ? `Restart now?` : null,
status === 'paused' ? `Resume text checking?` : null,
]
.filter(Boolean)
.join('\n')
this.#statusbar.command = status === 'error' ? 'grammarly.restartServer' : undefined
this.#statusbar.command =
status === 'error'
? { title: 'Restart', command: 'grammarly.restartServer' }
: status === 'paused'
? { title: 'Resume', command: 'grammarly.resumeCheck', arguments: [document.uri] }
: { title: 'Pause', command: 'grammarly.pauseCheck', arguments: [document.uri] }
this.#statusbar.show()
await commands.executeCommand('setContext', 'grammarly.isActive', true)
await commands.executeCommand('setContext', 'grammarly.isPaused', status === 'paused')
}

private async hide() {
this.#statusbar.hide()
await commands.executeCommand('setContext', 'grammarly.isActive', false)
}
}
5 changes: 3 additions & 2 deletions packages/grammarly-languageclient/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { BaseLanguageClient } from 'vscode-languageclient'
import type { SessionStatus, SuggestionId } from '@grammarly/sdk'

export interface Protocol {
pause(): Promise<void>
getDocumentStatus(uri: string): Promise<SessionStatus | null>
pause(uri: string): Promise<void>
resume(uri: string): Promise<void>
getDocumentStatus(uri: string): Promise<SessionStatus | 'paused' | null>
isUserAccountConnected(): Promise<boolean>
getOAuthUrl(oauthRedirectUri: string): Promise<string>
logout(): Promise<void>
Expand Down
1 change: 1 addition & 0 deletions packages/grammarly-languageserver/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export const GRAMMARLY_SDK = Symbol('GrammarlySDK')
export const TEXT_DOCUMENTS_FACTORY = Symbol('TextDocuments')
export const CLIENT = Symbol('ClientCapabilities')
export const CLIENT_INFO = Symbol('ClientInfo')
export const CLIENT_INITIALIZATION_OPTIONS = Symbol('ClientInitializationOptions')
export const SERVER = Symbol('ServerCapabilities')
export const CONNECTION = Symbol('Connection')
14 changes: 12 additions & 2 deletions packages/grammarly-languageserver/src/createLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ import type {
TextDocuments,
TextDocumentsConfiguration,
} from 'vscode-languageserver'
import { CLIENT, CLIENT_INFO, CONNECTION, GRAMMARLY_SDK, SERVER, TEXT_DOCUMENTS_FACTORY } from './constants'
import {
CLIENT,
CLIENT_INFO,
CLIENT_INITIALIZATION_OPTIONS,
CONNECTION,
GRAMMARLY_SDK,
SERVER,
TEXT_DOCUMENTS_FACTORY,
} from './constants'
import { CodeActionService } from './services/CodeActionService'
import { ConfigurationService } from './services/ConfigurationService'
import { DiagnosticsService } from './services/DiagnosticsService'
import { DocumentService } from './services/DocumentService'
import { HoverService } from './services/HoverService'
import type { SDK } from '@grammarly/sdk'
import { InitializationOptions } from './interfaces/InitializationOptions'

interface Disposable {
dispose(): void
Expand Down Expand Up @@ -56,13 +65,14 @@ export function createLanguageServer({
container.bind(SERVER).toConstantValue(capabilities)

connection.onInitialize(async (params) => {
const options = params.initializationOptions as { clientId: string } | undefined
const options = params.initializationOptions as InitializationOptions | undefined
if (options?.clientId == null) throw new Error('clientId is required')
await pathEnvironmentForSDK(options.clientId)
const sdk = await init(options.clientId)

container.bind(CLIENT).toConstantValue(params.capabilities)
container.bind(CLIENT_INFO).toConstantValue({ ...params.clientInfo, id: options.clientId })
container.bind(CLIENT_INITIALIZATION_OPTIONS).toConstantValue(options)
container.bind(GRAMMARLY_SDK).toConstantValue(sdk)
container.bind(TEXT_DOCUMENTS_FACTORY).toConstantValue(createTextDocuments)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface InitializationOptions {
clientId: string
startTextCheckInPausedState?: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export class DiagnosticsService implements Registerable {
public register(): Disposable {
this.#documents.onDidOpen((document) => this.#setupDiagnostics(document))
this.#documents.onDidClose((document) => this.#clearDiagnostics(document))
this.#connection.onRequest('$/pause', ([uri]: [uri: string]) => {
const document = this.#documents.get(uri)
if (document == null) return
document.pause()
this.#sendDiagnostics(document)
})
this.#connection.onRequest('$/resume', ([uri]: [uri: string]) => {
const document = this.#documents.get(uri)
if (document == null) return
document.resume()
this.#sendDiagnostics(document)
})
return { dispose() {} }
}

Expand All @@ -48,12 +60,7 @@ export class DiagnosticsService implements Registerable {
#setupDiagnostics(document: GrammarlyDocument) {
this.#connection.console.log(`${document.session.status} ${document.original.uri}`)
const diagnostics = new Map<SuggestionId, SuggestionDiagnostic>()
const sendDiagnostics = (): void => {
this.#connection.sendDiagnostics({
uri: document.original.uri,
diagnostics: Array.from(diagnostics.values()).map((item) => item.diagnostic),
})
}
const sendDiagnostics = (): void => this.#sendDiagnostics(document)

this.#diagnostics.set(document.original.uri, diagnostics)
document.session.addEventListener('suggestions', (event) => {
Expand Down Expand Up @@ -87,6 +94,15 @@ export class DiagnosticsService implements Registerable {
})
}

#sendDiagnostics(document: GrammarlyDocument) {
const diagnostics = this.#diagnostics.get(document.original.uri) ?? new Map()

this.#connection.sendDiagnostics({
uri: document.original.uri,
diagnostics: document.isPaused ? [] : Array.from(diagnostics.values()).map((item) => item.diagnostic),
})
}

#clearDiagnostics(document: GrammarlyDocument): void {
this.#connection.sendDiagnostics({
uri: document.original.uri,
Expand Down
24 changes: 22 additions & 2 deletions packages/grammarly-languageserver/src/services/DocumentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import type {
import type { Range, TextDocumentContentChangeEvent } from 'vscode-languageserver-textdocument'
import { TextDocument } from 'vscode-languageserver-textdocument'
import Parser from 'web-tree-sitter'
import { CONNECTION, GRAMMARLY_SDK, SERVER, TEXT_DOCUMENTS_FACTORY } from '../constants'
import { CLIENT_INITIALIZATION_OPTIONS, CONNECTION, GRAMMARLY_SDK, SERVER, TEXT_DOCUMENTS_FACTORY } from '../constants'
import { InitializationOptions } from '../interfaces/InitializationOptions'
import { Registerable } from '../interfaces/Registerable'
import { SourceMap } from '../interfaces/SourceMap'
import { Transformer } from '../interfaces/Transformer'
Expand All @@ -31,18 +32,21 @@ export class DocumentService implements Registerable {
@inject(SERVER) capabilities: ServerCapabilities,
@inject(GRAMMARLY_SDK) sdk: SDK,
@inject(TEXT_DOCUMENTS_FACTORY) createTextDocuments: <T>(config: TextDocumentsConfiguration<T>) => TextDocuments<T>,
@inject(CLIENT_INITIALIZATION_OPTIONS) options: InitializationOptions,
config: ConfigurationService,
) {
this.#connection = connection
this.#capabilities = capabilities
this.#config = config
this.#documents = createTextDocuments({
create(uri, languageId, version, content) {
return new GrammarlyDocument(TextDocument.create(uri, languageId, version, content), async () => {
const document = new GrammarlyDocument(TextDocument.create(uri, languageId, version, content), async () => {
const options = await config.getDocumentSettings(uri)
connection.console.log(`create text checking session for "${uri}" with ${JSON.stringify(options, null, 2)} `)
return sdk.withText({ ops: [] }, options)
})
if (options.startTextCheckInPausedState === true) document.pause()
return document
},
update(document, changes, version) {
document.update(changes, version)
Expand All @@ -62,6 +66,7 @@ export class DocumentService implements Registerable {
this.#connection.onRequest('$/getDocumentStatus', async ([uri]: [uri: string]) => {
const document = this.#documents.get(uri)
if (document == null) return null
if (document.isPaused) return 'paused'
await document.isReady()
return document.session.status
})
Expand Down Expand Up @@ -147,6 +152,20 @@ export class GrammarlyDocument {
}

private _isReady: Promise<void> | null = null
private _isPaused = false

get isPaused(): boolean {
return this._isPaused
}

public pause(): void {
this._isPaused = true
}

public resume(): void {
this._isPaused = false
this.#sync()
}

public async isReady(): Promise<void> {
if (this._isReady != null) await this._isReady
Expand Down Expand Up @@ -230,6 +249,7 @@ export class GrammarlyDocument {
}

#sync(): void {
if (this._isPaused) return
if (this.#context != null) {
const [text, map] = this.#context.transformer.encode(this.#context.tree)
this.session.setText(text)
Expand Down
Loading

0 comments on commit 75fce63

Please sign in to comment.