Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
sqs committed Jan 8, 2024
1 parent 3fd9dda commit 5e2c9d3
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 39 deletions.
2 changes: 1 addition & 1 deletion client/codemirror/src/blockWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function computeDecorations(
const annotationsByLine: { line: number; annotations: Annotation[] }[] = []
for (const ann of prepareAnnotationsForPresentation(annotations)) {
let cur = annotationsByLine.at(-1)
const startLine = ann.ui?.presentationHints?.includes('group-at-top-of-file') ? 0 : ann.range?.start.line ?? 0
const startLine = ann.ui?.presentationHints?.includes('show-at-top-of-file') ? 0 : ann.range?.start.line ?? 0
if (!cur || cur.line !== startLine) {
cur = { line: startLine, annotations: [] }
annotationsByLine.push(cur)
Expand Down
9 changes: 9 additions & 0 deletions client/vscode/src/controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { MockedObject, vi } from 'vitest'
import { Controller } from './controller'

export function createMockController(): MockedObject<Controller> {
return {
observeAnnotations: vi.fn(),
onDidChangeProviders: vi.fn(),
}
}
168 changes: 168 additions & 0 deletions client/vscode/src/ui/editor/codeLens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { Annotation } from '@opencodegraph/client'
import { TestScheduler } from 'rxjs/testing'
import { describe, expect, MockedObject, test, vi } from 'vitest'
import type * as vscode from 'vscode'
import { URI } from 'vscode-uri'
import { Controller } from '../../controller'
import { createMockController } from '../../controller.test'
import { createPosition, createRange, mockTextDocument } from '../../util/vscode.test'
import { createCodeLensProvider } from './codeLens'

type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>
}

vi.mock(
'vscode',
() =>
({
Range: function (): vscode.Range {
return createRange(0, 0, 0, 0)
} as any,
commands: {
registerCommand: vi.fn(),
},
EventEmitter: function () {
return { event: null }
} as any,
Uri: URI,
}) satisfies RecursivePartial<typeof vscode>
)

function fixtureResult(label: string): Annotation<vscode.Range> {
return {
title: label.toUpperCase(),
range: createRange(0, 0, 0, 1),
}
}

function createTestProvider(): {
controller: MockedObject<Controller>
provider: ReturnType<typeof createCodeLensProvider>
} {
const controller = createMockController()
return { controller, provider: createCodeLensProvider(controller) }
}

describe('createCodeLensProvider', () => {
const testScheduler = (): TestScheduler =>
new TestScheduler((actual, expected) => expect(actual).toStrictEqual(expected))

test('simple', () => {
const { controller, provider } = createTestProvider()
const doc = mockTextDocument()
testScheduler().run(({ cold, expectObservable }): void => {
controller.observeAnnotations.mockImplementation(doc => {
expect(doc).toBe(doc)
return cold<Annotation<vscode.Range>[] | null>('a', { a: [fixtureResult('a')] })
})
expectObservable(provider.observeCodeLenses(doc)).toBe('a', {
a: [{ isResolved: true, range: createRange(0, 0, 0, 1), command: { title: 'A', command: 'noop' } }],
} satisfies Record<string, vscode.CodeLens[] | null>)
})
})

test('detail hover', () => {
const { controller, provider } = createTestProvider()
const doc = mockTextDocument()
testScheduler().run(({ cold, expectObservable }): void => {
controller.observeAnnotations.mockImplementation(doc =>
cold<Annotation<vscode.Range>[] | null>('a', { a: [{ title: 'A', ui: { detail: 'D' } }] })
)
expectObservable(provider.observeCodeLenses(doc)).toBe('a', {
a: [
{
isResolved: true,
range: createRange(0, 0, 0, 0),
command: {
title: 'A',
command: 'opencodegraph._showHover',
arguments: [doc.uri, createPosition(0, 0)],
},
},
],
} satisfies Record<string, vscode.CodeLens[] | null>)
})
})

test('prefer-link-over-detail', () => {
const { controller, provider } = createTestProvider()
const doc = mockTextDocument()
testScheduler().run(({ cold, expectObservable }): void => {
controller.observeAnnotations.mockImplementation(doc =>
cold<Annotation<vscode.Range>[] | null>('a', {
a: [
{
title: 'A',
url: 'https://example.com',
ui: { detail: 'D', presentationHints: ['prefer-link-over-detail'] },
},
],
})
)
expectObservable(provider.observeCodeLenses(doc)).toBe('a', {
a: [
{
isResolved: true,
range: createRange(0, 0, 0, 0),
command: {
title: 'A',
command: 'vscode.open',
arguments: [URI.parse('https://example.com')],
},
},
],
} satisfies Record<string, vscode.CodeLens[] | null>)
})
})

test('show-at-top-of-file', () => {
const { controller, provider } = createTestProvider()
const doc = mockTextDocument()
testScheduler().run(({ cold, expectObservable }): void => {
controller.observeAnnotations.mockImplementation(doc =>
cold<Annotation<vscode.Range>[] | null>('a', {
a: [
{
title: 'A',
ui: { detail: 'D', presentationHints: ['show-at-top-of-file'] },
range: createRange(1, 2, 3, 4),
},
],
})
)
expectObservable(provider.observeCodeLenses(doc)).toBe('a', {
a: [
{
isResolved: true,
range: createRange(0, 0, 0, 0),
command: {
title: 'A',
command: 'opencodegraph._showHover',
arguments: [doc.uri, createPosition(1, 2)],
},
},
],
} satisfies Record<string, vscode.CodeLens[] | null>)
})
})

test.skip('grouped', () => {
const { controller, provider } = createTestProvider()
const doc = mockTextDocument()
testScheduler().run(({ cold, expectObservable }): void => {
controller.observeAnnotations.mockImplementation(doc => {
expect(doc).toBe(doc)
return cold<Annotation<vscode.Range>[] | null>('a', {
a: [
{ title: 'A', range: createRange(1, 0, 1, 0), ui: { group: 'G' } },
{ title: 'B', range: createRange(1, 0, 1, 0), ui: { group: 'G' } },
],
})
})
expectObservable(provider.observeCodeLenses(doc)).toBe('a', {
a: [{ isResolved: true, range: createRange(1, 0, 1, 0), command: { command: 'noop', title: 'A' } }],
} satisfies Record<string, vscode.CodeLens[] | null>)
})
})
})
40 changes: 23 additions & 17 deletions client/vscode/src/ui/editor/codeLens.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { prepareAnnotationsForPresentation } from '@opencodegraph/ui-common'
import { type AnnotationWithAdjustedRange } from '@opencodegraph/ui-common/src/ui'
import { firstValueFrom, map } from 'rxjs'
import { firstValueFrom, map, type Observable } from 'rxjs'
import * as vscode from 'vscode'
import { makeRange, type Controller } from '../../controller'

interface CodeLens extends vscode.CodeLens {}

export function createCodeLensProvider(controller: Controller): vscode.CodeLensProvider<CodeLens> & vscode.Disposable {
export function createCodeLensProvider(controller: Controller): vscode.CodeLensProvider<CodeLens> & {
observeCodeLenses(doc: vscode.TextDocument): Observable<CodeLens[]>
} & vscode.Disposable {
const disposables: vscode.Disposable[] = []

const showHover = createShowHoverCommand()
Expand All @@ -17,40 +19,44 @@ export function createCodeLensProvider(controller: Controller): vscode.CodeLensP

disposables.push(controller.onDidChangeProviders(() => changeCodeLenses.fire()))

return {
const provider = {
onDidChangeCodeLenses: changeCodeLenses.event,
async provideCodeLenses(doc: vscode.TextDocument): Promise<CodeLens[]> {
return firstValueFrom(
controller.observeAnnotations(doc).pipe(
map(anns => {
if (anns === null) {
return []
}
return prepareAnnotationsForPresentation<vscode.Range>(anns, makeRange).map(ann =>
createCodeLens(doc, ann, showHover)
)
})
),
{ defaultValue: [] }
observeCodeLenses(doc: vscode.TextDocument): Observable<CodeLens[]> {
return controller.observeAnnotations(doc).pipe(
map(anns => {
if (anns === null) {
return []
}
return prepareAnnotationsForPresentation<vscode.Range>(anns, makeRange).map(ann =>
createCodeLens(doc, ann, showHover)
)
})
)
},
async provideCodeLenses(doc: vscode.TextDocument): Promise<CodeLens[]> {
return firstValueFrom(provider.observeCodeLenses(doc), { defaultValue: [] })
},
dispose() {
for (const disposable of disposables) {
disposable.dispose()
}
},
}
return provider
}

function createCodeLens(
doc: vscode.TextDocument,
ann: AnnotationWithAdjustedRange<vscode.Range>,
showHover: ReturnType<typeof createShowHoverCommand>
): CodeLens {
// If the presentationHint `group-at-top-of-file` is used, show the code lens at the top of the
// If the presentationHint `show-at-top-of-file` is used, show the code lens at the top of the
// file, but make it trigger the hover at its actual location.
const attachRange = ann.range ?? new vscode.Range(0, 0, 0, 0)
const hoverRange = ann.originalRange ?? attachRange
if (ann.originalRange) {
console.log('XX1')
}
return {
range: attachRange,
command: {
Expand Down
37 changes: 37 additions & 0 deletions client/vscode/src/util/vscode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type * as vscode from 'vscode'
import { URI } from 'vscode-uri'

export function mockTextDocument(uri = 'file:///a.txt'): vscode.TextDocument {
return {
uri: URI.parse(uri),
languageId: 'plaintext',
getText: () => 'test',
} as vscode.TextDocument
}

export const noopCancellationToken: vscode.CancellationToken = null as any

export function createPosition(line: number, character: number): vscode.Position {
return {
line,
character,
} as vscode.Position
}

export function createRange(
startLine: number,
startCharacter: number,
endLine: number,
endCharacter: number
): vscode.Range {
return {
start: {
line: startLine,
character: startCharacter,
},
end: {
line: endLine,
character: endCharacter,
},
} as vscode.Range
}
13 changes: 2 additions & 11 deletions lib/client/src/client/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type AnnotationsParams, type AnnotationsResult } from '@opencodegraph/protocol'
import { type AnnotationsParams } from '@opencodegraph/protocol'
import { type Range } from '@opencodegraph/schema'
import { firstValueFrom, of } from 'rxjs'
import { TestScheduler } from 'rxjs/testing'
Expand All @@ -16,15 +16,6 @@ const FIXTURE_PARAMS: AnnotationsParams = {
content: 'A',
}

function fixtureProviderResult(label: string): AnnotationsResult {
return [
{
title: label.toUpperCase(),
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
},
]
}

function fixtureResult(label: string): Annotation {
return {
title: label.toUpperCase(),
Expand Down Expand Up @@ -82,7 +73,7 @@ describe('Client', () => {
a: { enable: true, providers: { [testdataFileUri('simple.js')]: {} } },
}),
__mock__: {
getProviderClient: () => ({ annotations: () => of(fixtureProviderResult('a')) }),
getProviderClient: () => ({ annotations: () => of([fixtureResult('a')]) }),
},
}).annotationsChanges(FIXTURE_PARAMS)
).toBe('(0a)', { '0': [], a: [fixtureResult('a')] } satisfies Record<string, Annotation[]>)
Expand Down
4 changes: 2 additions & 2 deletions lib/schema/src/opencodegraph.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@
"type": "array",
"items": {
"title": "PresentationHint",
"description": "A hint about how to best present an annotation to the human in the client's user interface.\n\n- `group-at-top-of-file`: Group all annotations with the same `ui.group` value together and display them at the top of the file instead of at their given file range.\n- `prefer-link-over-detail`: Prefer to show the annotation as a link over showing the detail text, if the client does not cleanly support doing both.",
"description": "A hint about how to best present an annotation to the human in the client's user interface.\n\n- `show-at-top-of-file`: Group all annotations with the same `ui.group` value together and display them at the top of the file instead of at their given file range.\n- `prefer-link-over-detail`: Prefer to show the annotation as a link over showing the detail text, if the client does not cleanly support doing both.",
"type": "string",
"enum": ["group-at-top-of-file", "prefer-link-over-detail"]
"enum": ["show-at-top-of-file", "prefer-link-over-detail"]
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions lib/schema/src/opencodegraph.schema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/**
* A hint about how to best present an annotation to the human in the client's user interface.
*
* - `group-at-top-of-file`: Group all annotations with the same `ui.group` value together and display them at the top of the file instead of at their given file range.
* - `show-at-top-of-file`: Group all annotations with the same `ui.group` value together and display them at the top of the file instead of at their given file range.
* - `prefer-link-over-detail`: Prefer to show the annotation as a link over showing the detail text, if the client does not cleanly support doing both.
*/
export type PresentationHint = 'group-at-top-of-file' | 'prefer-link-over-detail'
export type PresentationHint = 'show-at-top-of-file' | 'prefer-link-over-detail'

/**
* Metadata about code
Expand Down
4 changes: 2 additions & 2 deletions lib/ui-common/src/ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('prepareAnnotationsForPresentation', () => {
{
title: '📟 http_request_queue (metric)',
ui: {
presentationHints: ['group-at-top-of-file'],
presentationHints: ['show-at-top-of-file'],
},
range: {
start: { line: 3, character: 4 },
Expand All @@ -20,7 +20,7 @@ describe('prepareAnnotationsForPresentation', () => {
{
title: '📟 http_request_queue (metric)',
ui: {
presentationHints: ['group-at-top-of-file'],
presentationHints: ['show-at-top-of-file'],
},
range: {
start: { line: 0, character: 0 },
Expand Down
2 changes: 1 addition & 1 deletion lib/ui-common/src/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function prepareAnnotationsForPresentation<R extends Range = Range>(
): AnnotationWithAdjustedRange<R>[] {
return annotations
.map(ann => {
if (ann.ui?.presentationHints?.includes('group-at-top-of-file')) {
if (ann.ui?.presentationHints?.includes('show-at-top-of-file')) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
ann = {
...ann,
Expand Down
2 changes: 1 addition & 1 deletion provider/docs/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default multiplex<Settings>(async settings => {
detail: truncate(doc.content?.textContent || sr.excerpt, 200),
format: 'plaintext',
group: '📘 Docs',
presentationHints: ['group-at-top-of-file', 'prefer-link-over-detail'],
presentationHints: ['show-at-top-of-file', 'prefer-link-over-detail'],
},
range: {
start: positionCalculator(contentChunk.range.start),
Expand Down
Loading

0 comments on commit 5e2c9d3

Please sign in to comment.