question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Partial Semantic Editing Operations

See original GitHub issue

Background

We’ve heard from many TypeScript and JavaScript developers that large codebases suffer from massive startup delays in the editor. This is often due to program construction time. Before any semantic operation can be triggered, TypeScript has to effectively do the following:

  1. Find out which project a file belongs to
  2. Load that project
  3. loop: Read each of the given files
  4. Parse the file
  5. Look through the imports/exports and resolve each import path
  6. If new files were found, add them to the program’s file list and go to loop.

All this is unfortunately synchronous today, and even if it wasn’t, this might still end up being a lot of work for larger projects. For users with many files and non-trivial resolution schemes, we’ve witnessed program construction times that have taken minutes.

This can be frustrating for lots of users whose first tasks might just include navigation and some basic editing.

Basic Code Navigation

Let’s take the following code and assume a user requests to go-to-definition on the import statement:

import * as foo from "./foo";

If TypeScript is still loading the program, it can’t answer where ./foo resolves to.

But it’s a relative path, and for most users this feels like something so obvious and cheap to compute. Most users could guess which file it points to be right. If there was a lightweight way to say “it should probably take you here” until a program is fully loaded up, it might get 99% of the job done for 99% of people. If there was a way to guess 2-3 locations and give the editor a list of locations to look up, that might just solve the issue.

Basic Completions

Let’s take the following and assume the user requests completions in each of the comments:

import { SomeClass } from "some-module";

function foo() {
    function bar() {
        b/*inBar*/
    }
    function baz() {
        SomeClass./*afterDot*/
    }

    new /*afterNew*/
}

class CurrentClass {
    /*inClass*/
}

Completions at /*inBar*/ and /*afterNew*/ technically need a full program to tell us what globals are available, but a lot of the time users might be able to get by with locals until the program is fully loaded - after all, this was the basic experience in lots of editors before they had TypeScript support. We could still build up the list of local symbols and list those as the highest-priority completions, followed by a list of local identifiers.

When it comes to /*afterDot*/ something like what’s proposed in https://github.com/Microsoft/TypeScript/issues/5334 might be good enough - just provide a list of local identifiers.

A completion at /*inClass*/ could limit itself to contextual keywords (e.g. public, private, protected, readonly, abstract, whatever) and that would still be useful.

Proposal

Some editors spin up two TS Server instances: one which looks for full semantic information, and one which only answers file-local syntactic questions. So one idea we’ve had is: could we give the syntactic server some basic heuristics to provide answers to editor questions that appear obvious, or where incomplete answers are good enough?

The proposal is to enable syntax-only instances of TypeScript’s language service to answer a set of semantic-like questions that includes

  • Go-to-Definition on
    • relative paths
    • locally declared functions and variables
  • Completions based on
    • Locally scoped identifiers
    • Keywords based on different syntactic contexts

Go to Definition

On Imports to Relative Paths

When it comes to relative paths, there’s at least the following resolution scheme:

  • If it has an extension
    • remove it, try to resolve by appending .ts, .tsx, .d.ts, .js, .jsx
    • try loading the file by that path (e.g. .json files)
  • Try appending .ts, .tsx, .d.ts, .js, .jsx to the current path

TypeScript can potentially provide an ordered list of lookup locations back to the editor, or just guess at one. The editor can try all of these, wait, and return the first “preferred” definition, but we’d need to sync with editor teams to see how feasible this would be.

Alternatively, the syntax server can perform the work, but there are some complications. See the open questions below.

On Imported Bindings

In

import { foo } from "./foo";

/**/foo();

It might be okay for go-to-definition to be the same as go-to-definition for ./foo itself if it’s prohibitively expensive to load/parse/bind a file on the fly (though we’re going to do that once it’s opened in the editor anyway).

On Local Bindings

TypeScript can potentially bind each source file on demand, and walk up the AST to resolve the requested symbol, and jump to the definition. This means that go-to-definition definitely wouldn’t work on globals across files, and it might be able to jump to the correct file of certain imports, but it would at least definitely work within the current file.

Completions

Completions could work by providing the list of contextual keywords based on the current AST (which we already have at least in part), along with a list of either file-local identifier completions or whatever’s in the current chain of file-local symbol tables.

Editor Usage

Editors could use this new functionality by changing certain commands to be multi-step commands: see if the semantic language service is in the middle of loading a project first, use the syntactic version of the command, and then try to fire semantic a syntax command if that fails (e.g. for go-to-definition, none of the operations worked).

Alternatively, it can have both operations race against each other. Try both concurrently, see which completes first, and then either cancel the operation the semantic server if it loses or throw away the results of the syntax server if it loses.

Prior Art

Current Architecture

The syntax server existing in the first place gives some legitimacy to this approach - a server for answering questions that doesn’t know the entire state of the world.

Roslyn

Roslyn has some of the same concepts but with a different approach. Roslyn makes a lot of their architecture asynchronous and lazy, and if results aren’t currently available, they’ll provide an incomplete experience - for example, completion lists aren’t always available for the entire program. The approach is drastically different from what’s being proposed here, but it’s fundamentally the same idea: work with what you’ve got until you’ve got it all.

Open Questions

Can we return locations that don’t exist in navigation?

We mention above the idea of guessing file locations - but those files might not exist on disk! Some editors might not expect an invalid location to be returned. We’ll have to chat with editor teams to understand whether this is actually a problem or not. Without an opt-in flag, it might be.

Does go-to-definition need to return a ranked list of files to try?

Today, go-to-definition just returns a list of locations. In this version of go-to-definition, we might want to provide a list of potential lookup locations for the editor - but they’re not all equally weighted. You don’t want to provide the .js file in the same list of definitions as the .ts file. It seems like this version of go-to-definition needs to be slightly altered from the existing version.

Can the server just resolve the files itself instead returning a ranked list of files?

Technically it can but TSServer is synchronous. Maybe for a max of 5-10 file existence checks this wouldn’t be too bad, but we should figure out if it’s better for editors to decide.

Do we need new TSServer commands?

Maybe! Technically, we can have existing TS Server commands respond differently between the syntax server and the semantic server, but to be honest, I think we’d be better off not doing this. Some of this new functionality might need opt-in flags, so I think it would be better to make this entire experience opt-in via a separate language server command.

Is TypeScript providing its own list of completions actually better than most editors’ naive implementation of completions?

It’s not entirely clear whether TypeScript’s “look at every identifier in the file” approach is actually going to be better than what an editor like VS Code does in a plaintext file. We should understand what they do for plaintext and see if there’s actually anything we can provide that’s better.

Should editors ever fall back to sytnax operations once the semantic server is done?

Operations like go-to-definition can often trigger new project loads. This will block the semantic server again. It feels like using the syntax server is the right call, but this is another place where “not everything works perfectly” might be confusing.

Would this experience too different/incomplete?

It might be very jarring if a user got drastically different results from a normal editing session. Hopefully visual indicators from the editor can give a hint that results will be slightly incomplete, but a big goal here is to capture as much of the current experience in a lightweight way.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:10
  • Comments:14 (14 by maintainers)

github_iconTop GitHub Comments

3reactions
ahejlsbergcommented, Mar 31, 2020

I’m not sure I understand what “…and one which only answers file-local syntactic questions” really is? It seems almost any question other than syntax highlighting and grammatical errors requires some form of binding and semantic analysis, i.e. creation of a program and involvement of the type checker. I think a more viable approach is to just create a single file program for the current file where we squelch errors from unresolved references but otherwise just answer requests the same way as always. A number of things will become any, but some local stuff would still work.

1reaction
mihailikcommented, May 20, 2020

resolution happens at one big synchronous step

Isn’t it incremental? Such that when it’s happened once, an incremental update would be fast?

Tell me more - were you spinning up extra service instances to do this?

No, the same server. I fed it a series of lies, gradually approaching the truth. That is, LSHost.getScriptFileNames() was only giving the service a subset of files.

I add a file to the list, and invoke getSemanticDiagnostics(...) on it. That makes Program parse, resolve and all the rest except emitting. After a tiny delay I add another, invoke again, and so on. That file-by-file loading prevented freezing.

This was a while ago, here is a relevant part of that code:

image

Read more comments on GitHub >

github_iconTop Results From Across the Web

Semantics of editing operations | Download Scientific Diagram
This paper presents a novel editor supporting interactive refinement in the development of structured documents. The user performs a sequence of editing ......
Read more >
SESAME: Semantic Editing of Scenes by Adding ... - ECVA
In this work, we introduce SESAME a novel method for semantic image editing covering the complete spectrum of adding, manipulating, and erasing operations....
Read more >
Maintaining Semantic Information across Generic 3D Model ...
While semantic information is often lost during edit operations, geometry, UV mappings, and materials are usually maintained. This article presents a data model ......
Read more >
[2004.04977] SESAME: Semantic Editing of Scenes by Adding ...
They are not capable of handling the complete set of editing operations, that is addition, manipulation or removal of semantic concepts.
Read more >
The use of uniform semantic constraints in defining model-based ...
Such editing operations can be designed on t. ... Consequently, the editing can be partially automated, and the changes can more likely be...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found