microsoft / vscode

Visual Studio Code
https://code.visualstudio.com
MIT License
164.7k stars 29.45k forks source link

API support for Timeline view #84297

Open eamodio opened 5 years ago

eamodio commented 5 years ago

Goals

Add support for a unified file-based timeline view to be added to the Explorer sidebar which will track the active document (similar to the Outline view). This new view can be contributed by multiple sources, e.g. save/undo points, source control commits, test runs/failures, etc. Events from all sources will be aggregated into a single chronologically-ordered view.

Proposal

    export class TimelineItem {
        /**
         * A date for when the timeline item occurred
         */
        date: Date;

        /**
         * A human-readable string describing the source of the timeline item. This can be used for filtering by sources so keep it consistent across timeline item types.
         */
        source: string;

        /**
         * Optional method to get the children of the timeline item (if any).
         *
         * @return Children of the timeline item (if any).
         */
        getChildren?(): ProviderResult<TreeItem[]>;

        /**
         * A human-readable string describing the timeline item. When `falsy`, it is derived from [resourceUri](#TreeItem.resourceUri).
         */
        label: string;

        /**
         * Optional id for the timeline item. See [TreeItem.id](#TreeItem.id) for more details.
         */
        id?: string;

        /**
         * The icon path or [ThemeIcon](#ThemeIcon) for the timeline item. See [TreeItem.iconPath](#TreeItem.iconPath) for more details.
         */
        iconPath?: string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon;

        /**
         * A human readable string describing less prominent details of the timeline item. See [TreeItem.description](#TreeItem.description) for more details.
         */
        description?: string | boolean;

        /**
         * The [uri](#Uri) of the resource representing the timeline item (if any). See [TreeItem.resourceUri](#TreeItem.resourceUri) for more details.
         */
        resourceUri?: Uri;

        /**
         * The tooltip text when you hover over the timeline item.
         */
        tooltip?: string | undefined;

        /**
         * The [command](#Command) that should be executed when the timeline item is selected.
         */
        command?: Command;

        /**
         * [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the timeline item.
         */
        collapsibleState?: TreeItemCollapsibleState;

        /**
         * Context value of the timeline item.  See [TreeItem.contextValue](#TreeItem.contextValue) for more details.
         */
        contextValue?: string;

        /**
         * @param label A human-readable string describing the timeline item
         * @param date A date for when the timeline item occurred
         * @param source A human-readable string describing the source of the timeline item
         * @param collapsibleState [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the timeline item. Default is [TreeItemCollapsibleState.None](#TreeItemCollapsibleState.None)
         */
        constructor(label: string, date: Date, source: string, collapsibleState?: TreeItemCollapsibleState);
    }

    export interface TimelimeAddEvent {

        /**
         * An array of timeline items which have been added.
         */
        readonly items: readonly TimelineItem[];

        /**
         * The uri of the file to which the timeline items belong.
         */
        readonly uri: Uri;
    }

    export interface TimelimeChangeEvent {

        /**
         * The date after which the timeline has changed. If `undefined` the entire timeline will be reset.
         */
        readonly since?: Date;

        /**
         * The uri of the file to which the timeline changed.
         */
        readonly uri: Uri;
    }

    export interface TimelineProvider {
        onDidAdd?: Event<TimelimeAddEvent>;
        onDidChange?: Event<TimelimeChangeEvent>;

        /**
         * Provide [timeline items](#TimelineItem) for a [Uri](#Uri) after a particular date.
         *
         * @param uri The uri of the file to provide the timeline for.
         * @param since A date after which timeline items should be provided.
         * @param token A cancellation token.
         * @return An array of timeline items or a thenable that resolves to such. The lack of a result
         * can be signaled by returning `undefined`, `null`, or an empty array.
         */
        provideTimeline(uri: Uri, since: Date, token: CancellationToken): ProviderResult<TimelineItem[]>;
    }

    export namespace workspace {
        /**
         * Register a timeline provider.
         *
         * Multiple providers can be registered. In that case, providers are asked in
         * parallel and the results are merged. A failing provider (rejected promise or exception) will
         * not cause a failure of the whole operation.
         *
         * @param selector A selector that defines the documents this provider is applicable to.
         * @param provider A timeline provider.
         * @return A [disposable](#Disposable) that unregisters this provider when being disposed.
         */
        export function registerTimelineProvider(selector: DocumentSelector, provider: TimelineProvider): Disposable;
    }

A timeline source (e.g. an extension) registers a TimelineProvider for a set of documents. VS Code will then call the all the registered TimelineProvider.provideTimeline callbacks (in parallel) when the active editor changes and matches those registrations. The results will be merged into a unified set ordered by the TimelineItem.date and displayed in a new File Timeline view in the Explorer sidebar.

A timeline provider can signal that new events have occurred via the onDidAdd event, providing the set of additional timeline items. A provider can also signal a refresh of its timeline items via the onDidChange event.

Questions & Challenges

API

Behavior

Refs: https://github.com/microsoft/vscode/issues/83995

eamodio commented 4 years ago

Latest Proposal

https://github.com/microsoft/vscode/blob/f2cc6fd3d8591b0754e58256ca3f97ca58c239a1/src/vs/vscode.proposed.d.ts#L1473-L1566

Notes

Questions

TODO

lukaszpolowczyk commented 4 years ago

@eamodio Will this API give access to click history in the document, selection history in the document?

I use this in my extension, but I have to remember the clicked places myself. And I know that VSCode has a history undo function (Go> Back) but also a selection history.

eamodio commented 4 years ago

@lukaszpolowczyk This API is more about allowing a source to provide timeline information for a given Uri (document for now, but probably folder and workspace in the future). And that provided information is then shown as a list in a new Timeline view. This will enable extensions (and core) to start providing different sources to the timeline -- which could be undo history, selection history, etc.

Although, in this first iteration we will be providing Git file history in the timeline.

eamodio commented 4 years ago

Also for this iteration this will only be available in insiders and you will need to add the following to your settings.json: "timeline.showView": true

gjsjohnmurray commented 4 years ago

@eamodio wrote in the TODO above:

Add filtering support by source in the UI (and maybe backed by a setting)

Maybe worth the API comment for source recommending that an extension contributing to a timeline uses their extension ID (i.e. <publisher>.<name> from their manifest) as source or as a prefix for it, perhaps separated by /. For example: acmecorp.dynamite or for greater granularity acmecorp.dynamite/major, acmecorp.dynamite/minor.

Though if the timeline can already obtain the ID of the extension implementing the TimelineProvider that gave it the items it won't be necessary to ask providers to use sources that are structured so as to avoid conflicts.

eamodio commented 4 years ago

@gjsjohnmurray Yeah, I'm not sure if the source needs to be namespaced per-extension (though if we do end up wanting that, I would just do it internally). Currently with the replaceable property the desire is to be able to override a source with another one. But I still need to think more about that -- since I might just get rid of replaceable completely and rely solely on filtering (rather than overriding) and if so, then namespacing is probably appropriate.

Zalastax commented 4 years ago

@amelio-vazquez-reina asks if this could be used to support a tree based undo history https://github.com/microsoft/vscode/issues/20889#issuecomment-583194444:

Could the the new Timeline view be used to support this feature? Or does it only support "path graphs" (aka "linear graphs"), i.e. graphs where nodes can have at most one predecessor and at most one successor?

Note that the "undo graph" (aka "history graph" aka "undo branches") is usually at least a tree, if not a general graph (if we assume a graph where any nodes with the same text collapse into one node)

eamodio commented 4 years ago

@Zalastax @amelio-vazquez-reina Currently the timeline is linear, but we are thinking about allowing each linear node to expand to contain a set of child nodes. Although the current thinking is that there will only be 1 level of children.

For example, if you were to show the Git history of a folder, then each linear item would be a commit, which would expand to show the set of files within that commit.

amelio-vazquez-reina commented 4 years ago

Thanks @eamodio - What would you say is the main argument against supporting history trees in the Timeline view? is it the complexity and effort that it may require to do so while properly delivering a great / easy / intuitive UX for the user? Or are there any other reasons?

In my mind, there are good UX designs that could accommodate tree views and make them optional if needed. In case it helps, below is a screenshot and animation of a similar feature (for navigating the full undo history of a file) in Emacs:

Screenshot:

4vbd9

Video example

new

Description of the animation: The video tries to be self-explanatory, but just in case, here's a detailed description. The user starts typing text on a buffer (editor in VScode), then undoing some of the typing, and then typing text again. At a specific point in time the user invokes a command called "undo-tree" (equivalent to our Timeline view) which splits the screen in half and shows:

Once the undo tree is visible, the user switches cursor focus to the tree in the lower half, and the user navigates up and down the tree (i.e. history of the original file) via keyboard shortcuts. As the user is visiting different nodes, the original file (at the top) is updating its contents to reflect the state of the file corresponding to that node, and at any point in time, the user can switch focus to the original file in the top half, and continue editing from that state.

Note also that when the focus is on the tree, the user can switch navigation branches in the tree from a node with multiple children. Doing so doesn't change the "current node" where the user is sitting, it just simply enables a different path the user can take via up/down.

Something that is not shown in this video is that the undo tree is not persisted anywhere (it's actually lost when a file is closed). But that's ok. The undo history of a file in most editors (VSCode included?) isn't really saved, since it only refers to an "editing session" while the user has both the editor and file open.

eamodio commented 4 years ago

I can't say there is a specific argument against any usage, but we do want to keep things consistent and intuitive from a UX perspective, and avoid unnecessary complexity (in both the UI/UX as well as the API). We also want to make sure the Timeline view is accessible and not overwhelming especially when mixing different sources. At the same time, we are certainly open to evolving the UI/UX/API in response to new valuable use-cases.

For history trees, how would you envision that would look and behave? What would need to change to support it?

Also I'm sure I'm missing something, but I don't understand what you are trying to show in the video example.

amelio-vazquez-reina commented 4 years ago

Thanks @eamodio Very helpful.

For history trees, how would you envision that would look and behave? What would need to change to support it?

Sorry the gif I uploaded above was incomplete. I have now fixed it in my last comment above and included a long description of the video to make it all more self-contained / clear. I hope that answers the question! Please let me know otherwise. Thanks again.

zabel-xyz commented 4 years ago

Good idea for such a feature ! I made my own tree in my extension local-history. I'll contribute to this timeline view...

JohnstonCode commented 4 years ago

This is a nice feature. Not ran into any issues using it for svn commit history

eamodio commented 4 years ago

Here is the latest proposal:

https://github.com/microsoft/vscode/blob/604fb7637f58fb3d118fcf2460b800836494fd5d/src/vs/vscode.proposed.d.ts#L1814-L1966

TimelineOptions is now much simpler:

export interface TimelineOptions {
    /**
     * A provider-defined cursor specifying the starting point of the timeline items that should be returned.
     */
    cursor?: string;

    /**
     * An optional maximum number timeline items or the all timeline items newer (inclusive) than the timestamp or id that should be returned.
     * If `undefined` all timeline items should be returned.
     */
    limit?: number | { timestamp: number; id?: string };
}

These are the usage combinations:

I've also simplified the paging property of the Timeline:

export interface Timeline {
    readonly paging?: {
        /**
         * A provider-defined cursor specifying the starting point of timeline items which are after the ones returned.
         * Use `undefined` to signal that there are no more items to be returned.
         */
        readonly cursor: string | undefined;
    }

    /**
     * An array of [timeline items](#TimelineItem).
     */
    readonly items: readonly TimelineItem[];
}

Now you only return paging if the results are paged, and you must either return a cursor to get the next set of items or undefined if there are no more items.

alekseyt commented 4 years ago

And if you have ideas on other types of information you'd like to see in this view, let us know!

This is really good stuff. This is probably not the point of this feature but it would be great to see this developing over time into full commit history (like gitk) and/or history navigation (like switching back and forth between revisions of a file (+ blame (+ comment))).

Even without those it's a great stuff, one less reason to switch to terminal, great job.

JohnstonCode commented 4 years ago

When will this API be moved from proposed?

JacksonKearl commented 4 years ago

Mentioned over here https://github.com/microsoft/vscode/issues/94155#issuecomment-607636707 but putting it in the main api-proposal too for visibility:

I think perhaps this would be easier to implement if it were like:

interface TimelineProvider {
   /* existing properties */

  // Get most recent `items` timelime items, starting after `start` if defined
  provideTimeline(uri: Uri, options: TimelineOptions, token: CancellationToken, items: number, start?: string): ProviderResult<Timeline>

  // Refresh items up to and including `cursor`
  updateTimeline?(uri: Uri, options: TimelineOptions, token: CancellationToken, cursor: string): ProviderResult<Timeline>

  // Not sure what this does tbh. Maybe not needed at all?
  fetchEntireTimeline?(...)
}

This way extensions can implement a minimal subset of the functionality and we can detect that in core and work around it. For instance, providers can minimally implement just provideTimeline. In that case, if they trigger a refresh the user will just get a single page back. If they go on to implement updateTimeline, then the user can get their full set of results back. I'm not really sure what the use case is for fetchEntireTimeline, but similar for that.

Right now when implementing one needs to perform a lot of runtime type checking and it's not clear to the implementor what the different cursor overloads are. If the method names made clear what the results they should return, rather than one needing to know how to associate cursor types with runtime behaviour, it'd be a lot more clear how to implement.

eamodio commented 4 years ago

@JohnstonCode It might be this iteration, but more likely the next one.

@JacksonKearl I'm still really not in favor of something like this -- to me it just actually increases the complexity and when fully implementing it I would just have all those methods delegate down to the same underlying method.

The way it is now, a Timeline provider is pretty much in control over what they implement. If they don't want to implement paging or cursor, they can just ignore them and never return a paging object in the result. And the limit properties are guidelines, not absolute, so even if we ask for 20 items, and you always return all of your items, that is fine -- we will handle it appropriately.

I am thinking about a way to ease some of the confusion with cursor and limit. Because at a basic level, the cursor defines a starting point, and limit defines an ending point. So hopefully with some minor terminology changes, or slight shape tweaks this will become much clearer.

eamodio commented 4 years ago

📢 Anyone currently using/trying this API or planning on it please join us in the April Extension Authors call: https://github.com/microsoft/vscode/issues/96927. We will be talking about the Timeline API and would love your feedback as we plan to finalize the API.

mjcrouch commented 4 years ago

Hi - I'm implementing this for perforce - is there a way to identify that a user has explicitly requested a refresh?

I don't really want to hit the server for the file log every time we switch tabs (hence I've implemented a cache), but I would think if someone has pressed the refresh button then we would want to force an update to the state.

For example, if another person has submitted the file the user is working on, this is totally asynchronous in perforce and we don't really have a way of detecting that this has happened, as a user I wouldn't necessarily expect to immediately see that revision, but I would want to be able to click on the refresh button and see that new revision appear.

my code so far

(as there's not really support for paging in perforce, just a limit from the latest, I implemented it so that it only gets limit results on the initial call, to reduce overhead of opening a file, then on any other call it gets the rest of the results. It did take me a while to understand the concepts of cursor and limit and how they would be used - without looking at the internal code it's rather opaque. so the docs will need some good examples of exactly when each combination of TimelineOptions is used)

p.s. I suspect you already know this but the default null setting for timeline.pageSize shows an error in the settings view, and when you click to fix it with json, it automatically sets it to zero which results in an empty timeline view

joaomoreno commented 4 years ago

@eamodio Since you're out on vacation, I'll move this forward.

joaomoreno commented 4 years ago

@eamodio Since you're out on vacation, I'll move this forward again. 😆

InTheCloudDan commented 3 years ago

This is just feedback from my very initial usage of the API, if detail supported MarkdownString similar to how the Treeview does now, for our use case it would allow us to show much more feature rich contextual info.

eamodio commented 3 years ago

@InTheCloudDan Can you please open a new issue for that -- I think it is a great idea.

gjsjohnmurray commented 3 years ago

@lszomoru can this be considered for finalization?

JacksonKearl commented 3 years ago

@gjsjohnmurray the remaining unknown is in dealing with workspace scoped timeline events. The timeline view would ideally have a way to present a full git log of the open repo(s), but right now it only works for single resources. My understanding is that we don't want to move forward with finalization until we have an answer for that.

kraabrsg commented 6 months ago

Hi, are there any plans to bring this into a release Version, we would eagerly like to use it.

Thanks!

gjsjohnmurray commented 6 months ago

Ahead of finalization of this API maybe revisit #147304 which acknowledges it's currently exposing two undocumented TimelineOptions properties cacheResult and resetCache to extensions.

InTheCloudDan commented 3 weeks ago

Any update, a timeline API would be helpful.