Merge history from vscode-language-fidl

Change-Id: I7fc7e4e9a46f2e084e3d3efc9530da18165a4907
diff --git a/.gitignore b/.gitignore
index 21d8cfd..0785e8e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+*.pyc
+__pycache__
+
 node_modules/
 out/
 /language-fidl-*.vsix
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..c2a4eac
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,8 @@
+# This is the list of Fuchsia Authors.
+
+# Names should be added to this file as one of
+#     Organization's name
+#     Individual's name <submission email address>
+#     Individual's name <submission email address> <email2> <emailN>
+
+Google Inc.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..81e2938
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,9 @@
+This repository accepts contributions using Gerrit.
+
+Instructions for using Gerrit:
+
+ * https://gerrit-review.googlesource.com/Documentation/
+
+Before we can land your change, you need to sign the Google CLA:
+
+ * https://cla.developers.google.com/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6d6ddbc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//    * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//    * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//    * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/PATENTS b/PATENTS
new file mode 100644
index 0000000..2746e78
--- /dev/null
+++ b/PATENTS
@@ -0,0 +1,22 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Fuchsia project.
+
+Google hereby grants to you a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this
+section) patent license to make, have made, use, offer to sell, sell,
+import, transfer, and otherwise run, modify and propagate the contents
+of this implementation of Fuchsia, where such license applies only to
+those patent claims, both currently owned by Google and acquired in
+the future, licensable by Google that are necessarily infringed by
+this implementation. This grant does not include claims that would be
+infringed only as a consequence of further modification of this
+implementation. If you or your agent or exclusive licensee institute
+or order or agree to the institution of patent litigation or any other
+patent enforcement activity against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that this
+implementation of Fuchsia constitutes direct or contributory patent
+infringement, or inducement of patent infringement, then any patent
+rights granted to you under this License for this implementation of
+Fuchsia shall terminate as of the date such litigation is filed.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8b90b74
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+Fuchsia Open Source Template Repository
+=======================================
+
+This repository is a template that we will use when creating new open source
+repositories for Fuchsia.
diff --git a/fidl-lsp/README.md b/fidl-lsp/README.md
new file mode 100644
index 0000000..7b051a3
--- /dev/null
+++ b/fidl-lsp/README.md
@@ -0,0 +1,101 @@
+# fidl-lsp
+
+`fidl-lsp` is an [LSP](https://microsoft.github.io/language-server-protocol/)
+Language Server for [FIDL](https://fuchsia.dev/fuchsia-src/development/languages/fidl).
+
+## Architecture
+
+The general flow of control in the language server goes like this:
+* A request/notification is received in `langserver/handler.go`
+* If it is a notification (changing state of the open files):
+  * The `LangHandler` tells the `FileSystem` to make the change
+  * The `LangHandler` triggers a re-analysis by the `Analyzer`
+* If it is a request for some language feature:
+  * The `LangHandler` dispatches the request to the `Analyzer`
+  * The `Analyzer` extracts the needed information and returns it
+  * The `LangHandler` sends the response to the client
+
+### `langserver` package
+
+This package includes the core logic of the language server. It deals with LSP specific boilerplate, JSON-RPC messages, etc. It includes the `LangHandler`
+type, which handles LSP requests, sends reponses and notifications to the
+client, and dispatches changes to the state manager or requests for analyses to the `Analyzer`.
+
+### `state` package
+
+The language server's state management is in the `state` package. Currently this is just an in memory file system (mapping of editor file names to file text).
+This could be wrapped in e.g. an `RWLock` to enable concurrent handling of LSP requests and notifications.
+
+### `analysis` package
+
+The "backend" of the server, which doesn't know about LSP but knows how
+to compile FIDL and analyze the JSON IR.
+
+Includes the `Analyzer` type, which maintains a set of compiled FIDL `Library`s
+and their constituent files, dependencies, and build artifacts. Main entry point
+is `Analyzer.Analyze()`, which is called every time a file is changed on the client. `Analyze` recompiles the relevant FIDL library for that file and imports  the JSON IR.
+
+The `Analyzer` compiles FIDL libraries by invoking `fidlc` in a separate
+process, gets diagnostics from `fidlc` and `fidl-lint`, and uses `fidl-format`
+for formatting. It locates dependencies using a `fidl_project.json` file, the
+path to which will be configurable in the LSP client extension. `fidl_project.json` declares all FIDL libraries the language server should be aware
+of, the paths to their constituent files, the path to their JSON IR,
+and their dependencies (by library name).
+
+## Supported LSP features
+
+This list documents LSP features supported by fidl-lsp.
+
+### General
+- [x] [initialize](https://microsoft.github.io/language-server-protocol/specification#initialize)
+- [x] [initialized](https://microsoft.github.io/language-server-protocol/specification#initialized)
+- [x] [shutdown](https://microsoft.github.io/language-server-protocol/specification#shutdown)
+- [ ] [exit](https://microsoft.github.io/language-server-protocol/specification#exit)
+- [ ] [$/cancelRequest](https://microsoft.github.io/language-server-protocol/specification#cancelRequest)
+
+### Workspace
+- [ ] [workspace/workspaceFolders](https://microsoft.github.io/language-server-protocol/specification#workspace_workspaceFolders)
+- [ ] [workspace/didChangeWorkspaceFolders](https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWorkspaceFolders)
+- [ ] [workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeConfiguration)
+- [ ] [workspace/configuration](https://microsoft.github.io/language-server-protocol/specification#workspace_configuration)
+- [ ] [workspace/didChangeWatchedFiles](https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWatchedFiles)
+- [ ] [workspace/symbol](https://microsoft.github.io/language-server-protocol/specification#workspace_symbol)
+- [ ] [workspace/applyEdit](https://microsoft.github.io/language-server-protocol/specification#workspace_applyEdit)
+
+### Text Synchronization
+- [x] [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specification#textDocument_didOpen)
+- [x] [textDocument/didChange](https://microsoft.github.io/language-server-protocol/specification#textDocument_didChange)
+- [ ] [textDocument/willSave](https://microsoft.github.io/language-server-protocol/specification#textDocument_willSave)
+- [ ] [textDocument/willSaveWaitUntil](https://microsoft.github.io/language-server-protocol/specification#textDocument_willSaveWaitUntil)
+- [ ] [textDocument/didSave](https://microsoft.github.io/language-server-protocol/specification#textDocument_didSave)
+- [ ] [textDocument/didClose](https://microsoft.github.io/language-server-protocol/specification#textDocument_didClose)
+
+### Diagnostics
+- [x] [textDocument/publishDiagnostics](https://microsoft.github.io/language-server-protocol/specification#textDocument_publishDiagnostics)
+
+### Language Features
+- [ ] [textDocument/completion](https://microsoft.github.io/language-server-protocol/specification#textDocument_completion)
+- [ ] [completionItem/resolve](https://microsoft.github.io/language-server-protocol/specification#completionItem_resolve)
+- [x] [textDocument/hover](https://microsoft.github.io/language-server-protocol/specification#textDocument_hover)
+- [ ] [textDocument/signatureHelp](https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp)
+- [ ] [textDocument/declaration](https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration)
+- [x] [textDocument/definition](https://microsoft.github.io/language-server-protocol/specification#textDocument_definition)
+- [ ] [textDocument/typeDefinition](https://microsoft.github.io/language-server-protocol/specification#textDocument_typeDefinition)
+- [ ] [textDocument/implementation](https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation)
+- [x] [textDocument/references](https://microsoft.github.io/language-server-protocol/specification#textDocument_references)
+- [ ] [textDocument/documentHighlight](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentHighlight)
+- [ ] [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol)
+- [ ] [textDocument/codeAction](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction)
+- [ ] [textDocument/selectionRange](https://github.com/Microsoft/language-server-protocol/issues/613)
+- [ ] [textDocument/codeLens](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens)
+- [ ] [codeLens/resolve](https://microsoft.github.io/language-server-protocol/specification#codeLens_resolve)
+- [x] [textDocument/documentLink](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentLink)
+- [ ] [documentLink/resolve](https://microsoft.github.io/language-server-protocol/specification#documentLink_resolve)
+- [ ] [textDocument/documentColor](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentColor)
+- [ ] [textDocument/colorPresentation](https://microsoft.github.io/language-server-protocol/specification#textDocument_colorPresentation)
+- [x] [textDocument/formatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting)
+- [ ] [textDocument/rangeFormatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_rangeFormatting)
+- [ ] [textDocument/onTypeFormatting](https://microsoft.github.io/language-server-protocol/specification#textDocument_onTypeFormatting)
+- [ ] [textDocument/rename](https://microsoft.github.io/language-server-protocol/specification#textDocument_rename)
+- [ ] [textDocument/prepareRename](https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename)
+- [ ] [textDocument/foldingRange](https://microsoft.github.io/language-server-protocol/specification#textDocument_foldingRange)
diff --git a/fidl-lsp/analysis/analyzer.go b/fidl-lsp/analysis/analyzer.go
new file mode 100644
index 0000000..8bbb4c7
--- /dev/null
+++ b/fidl-lsp/analysis/analyzer.go
@@ -0,0 +1,212 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"fmt"
+	"hash/fnv"
+	"io/ioutil"
+	"log"
+	"os"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+// Config keeps track of where the Analyzer can find FIDL tools, as well as the
+// root build directory, to which all locations in CompiledLibraries are
+// relative.
+// This config will be exposed where possible as settings in FIDL LSP client IDE
+// extensions.
+type Config struct {
+	BuildRootDir   string
+	FidlcPath      string
+	FidlLintPath   string
+	FidlFormatPath string
+}
+
+// A Library is a set of information about a FIDL library, including build
+// information (files and dependencies), the location of its JSON IR if it
+// exists (`json`), its deserialized JSON IR (`ir`), and any diagnostics on it.
+// `deps` and `files` are treated as sets.
+type Library struct {
+	deps map[fidlcommon.LibraryName]bool
+	// This library's constituent files in the form of absolute paths
+	files   map[string]bool
+	json    string
+	ir      *FidlLibrary // nil if not yet imported
+	diags   map[state.FileID][]Diagnostic
+	symbols symbolMap
+}
+
+// Analyzer compiles FIDL libraries and extracts analyses from them, including
+// diagnostics.
+// * inputFilesFIDL is a map from documents open in the editor to the absolute
+//   filepath of the corresponding temporary file that Analyzer uses as input
+//   for fidlc, etc.
+// * inputFileJSON is a map from FIDL library name to an absolute filepath to
+//   its JSON IR.
+type Analyzer struct {
+	// Where to find the fuchsia directory and FIDL tools.
+	cfg Config
+
+	// A map from library name (string), like "zx", to a set of data about that
+	// library: the FIDL files it includes, its JSON IR, its dependencies, its
+	// JSON IR, and diagnostics returned on it from fidlc and fidl-lint.
+	libs map[fidlcommon.LibraryName]*Library
+
+	// A map from open files to their corresponding tmp file used by the
+	// Analyzer as an input file for fidlc, fidl-lint, fidl-format, etc.
+	// Key is an lsp.DocumentURI, value is an absolute filepath.
+	inputFilesFIDL map[state.FileID]string
+
+	// A map from library name to the corresponding tmp file used for their JSON
+	// IR.
+	inputFilesJSON map[fidlcommon.LibraryName]string
+}
+
+// NewAnalyzer returns an Analyzer initialized with the set of CompiledLibraries
+// passed in.
+func NewAnalyzer(cfg Config, compiledLibraries CompiledLibraries) *Analyzer {
+	a := &Analyzer{
+		cfg:            cfg,
+		libs:           make(map[fidlcommon.LibraryName]*Library),
+		inputFilesFIDL: make(map[state.FileID]string),
+		inputFilesJSON: make(map[fidlcommon.LibraryName]string),
+	}
+	for name, lib := range compiledLibraries {
+		libName := fidlcommon.MustReadLibraryName(name)
+		a.libs[libName] = &Library{
+			deps:  make(map[fidlcommon.LibraryName]bool, len(lib.Deps)),
+			files: make(map[string]bool, len(lib.Files)),
+			json:  lib.JSON,
+		}
+		for _, dep := range lib.Deps {
+			a.libs[libName].deps[fidlcommon.MustReadLibraryName(dep)] = true
+		}
+		for _, file := range lib.Files {
+			a.libs[libName].files[file] = true
+		}
+	}
+	return a
+}
+
+// Analyze compiles and generates an analysis for the file at `path` in the
+// FileSystem. This currently includes compiling the library the file belongs to
+// and obtaining diagnostics on it.
+// Analyze only uses read access to the FileSystem.
+func (a *Analyzer) Analyze(fs *state.FileSystem, path state.FileID) error {
+	inputFilePath, err := a.writeFileToTmp(fs, path)
+	if err != nil {
+		return fmt.Errorf("failed to write file `%s` to tmp: %s", path, err)
+	}
+
+	// Add this file and its deps to its Library
+	// TODO: do this on setup with all files in workspace
+	file, err := fs.File(path)
+	if err != nil {
+		return fmt.Errorf("could not find file `%s`", path)
+	}
+	if libName, err := state.LibraryOfFile(file); err == nil {
+		if _, ok := a.libs[libName]; !ok {
+			a.libs[libName] = &Library{
+				deps:  make(map[fidlcommon.LibraryName]bool),
+				files: make(map[string]bool),
+				diags: make(map[state.FileID][]Diagnostic),
+			}
+		}
+
+		lib := a.libs[libName]
+		// If the file currently being edited was already part of lib.files,
+		// delete that and replace it with the temporary/edited version.
+		if absPath, err := state.FileIDToPath(path); err != nil {
+			log.Printf("error parsing: %s\n", err)
+		} else {
+			if _, ok := lib.files[absPath]; ok {
+				delete(lib.files, absPath)
+			}
+		}
+
+		lib.files[inputFilePath] = true
+		imports := state.ParsePlatformImportsMatch(file)
+		for _, dep := range imports {
+			lib.deps[fidlcommon.LibraryName(dep.Lib)] = true
+		}
+	}
+
+	// Compile the file's library
+	compileResult, err := a.compile(fs, path)
+	if err != nil {
+		return fmt.Errorf("error on analysis: %s", err)
+	}
+
+	a.libs[compileResult.lib].diags = compileResult.diags
+	return nil
+}
+
+// Cleanup deletes all temporary files Analyzer created during its lifetime to
+// compile FIDL libraries.
+func (a *Analyzer) Cleanup() {
+	for _, path := range a.inputFilesFIDL {
+		err := os.Remove(path)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
+		}
+	}
+	for _, path := range a.inputFilesJSON {
+		err := os.Remove(path)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err)
+		}
+	}
+}
+
+// writeFileToTmp writes the in-memory file indexed by `path` to a temporary
+// file on disk, in order to enable invoking some command on it in a separate
+// process, e.g. fidlc, fidl-format, etc.
+func (a *Analyzer) writeFileToTmp(fs *state.FileSystem, path state.FileID) (string, error) {
+	if _, ok := a.inputFilesFIDL[path]; !ok {
+		a.inputFilesFIDL[path] = fmt.Sprintf("/tmp/%d.fidl", hash(string(path)))
+	}
+
+	file, err := fs.File(path)
+	if err != nil {
+		return "", fmt.Errorf("could not find file `%s`", path)
+	}
+
+	// TODO: Can we do 0644 with os.FileMode(os.O_FLAGS)?
+	if err := ioutil.WriteFile(a.inputFilesFIDL[path], []byte(file), 0644); err != nil {
+		return "", fmt.Errorf("error writing file `%s` to tmp directory: %s", path, err)
+	}
+	return a.inputFilesFIDL[path], nil
+}
+
+func (a *Analyzer) inputFileToFileID(inputFilePath string) (state.FileID, error) {
+	for fileID, inputFile := range a.inputFilesFIDL {
+		if inputFile == inputFilePath {
+			return fileID, nil
+		}
+	}
+	return "", fmt.Errorf("could not find input file `%s`", inputFilePath)
+}
+
+// pathToJSON returns the path to the tmp file where the Analyzer saved the JSON
+// IR for the library `lib`, or, if there is not a saved JSON file, generates
+// a path. It is used for fidlc invocations and for importing libraries' JSON
+// IR.
+func (a *Analyzer) pathToJSON(lib fidlcommon.LibraryName) string {
+	if path, ok := a.inputFilesJSON[lib]; ok {
+		return path
+	}
+	a.inputFilesJSON[lib] = fmt.Sprintf("/tmp/%d.json", hash(lib.FullyQualifiedName()))
+	return a.inputFilesJSON[lib]
+}
+
+func hash(s string) uint32 {
+	h := fnv.New32a()
+	h.Write([]byte(s))
+	return h.Sum32()
+}
diff --git a/fidl-lsp/analysis/compile.go b/fidl-lsp/analysis/compile.go
new file mode 100644
index 0000000..9336999
--- /dev/null
+++ b/fidl-lsp/analysis/compile.go
@@ -0,0 +1,497 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os/exec"
+	"strconv"
+	"strings"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+type compileResult struct {
+	lib   fidlcommon.LibraryName
+	diags map[state.FileID][]Diagnostic
+}
+
+// Compile the FIDL library that includes the file at `path`.
+// Returns either a compileResult containing the name of the library along with
+// any diagnostics returned by fidlc and fidl-lint, or an error if compilation
+// fails.
+func (a *Analyzer) compile(fs *state.FileSystem, path state.FileID) (compileResult, error) {
+	var jsonPath string
+	file, err := fs.File(path)
+	if err != nil {
+		return compileResult{}, fmt.Errorf("could not find file `%s`", path)
+	}
+
+	libraryName, err := state.LibraryOfFile(file)
+	if err != nil {
+		return compileResult{}, fmt.Errorf("could not find library name of file `%s`: %s", path, err)
+	}
+	jsonPath = a.pathToJSON(libraryName)
+
+	args := append([]string{"--format=json"}, "--json", jsonPath)
+	files, err := a.FindDeps(fs, path)
+	if err != nil {
+		return compileResult{}, fmt.Errorf("could not find dependencies of file `%s`: %s", path, err)
+	}
+	args = append(args, files...)
+
+	cmd := exec.Command(a.cfg.FidlcPath, args...)
+	var stderr bytes.Buffer
+	cmd.Stderr = &stderr
+	if err := cmd.Run(); err == nil {
+		// If fidlc compiled successfully, update a.libs with the compiled JSON IR
+		if err := a.importLibrary(jsonPath); err != nil {
+			return compileResult{}, fmt.Errorf("error on adding compiled JSON: %s", err)
+		}
+	} else {
+		// TODO: there is no platform-independent way in Go to check an error
+		// status code, but if we can, we should somehow determine if fidlc
+		// exited with status code 1 (failure due to FIDL errors) or something
+		// else (internal error or command otherwise failed).
+		// If it's status code 1, we should continue, but anything else, we
+		// should return an error from `compile` to signal failure.
+	}
+
+	diags := make(map[state.FileID][]Diagnostic)
+	for fileName := range a.libs[libraryName].files {
+		fileID, err := a.inputFileToFileID(fileName)
+		if err == nil {
+			diags[fileID] = []Diagnostic{}
+		}
+	}
+	if errorsAndWarnings, err := a.fidlcDiagsFromStderr(stderr.Bytes()); err == nil {
+		for fileID, fileDiags := range errorsAndWarnings {
+			if _, ok := diags[fileID]; !ok {
+				diags[fileID] = []Diagnostic{}
+			}
+			diags[fileID] = append(diags[fileID], fileDiags...)
+		}
+	}
+	if lints, err := a.runFidlLint(a.inputFilesFIDL[path]); err == nil {
+		for fileID, fileDiags := range lints {
+			if _, ok := diags[fileID]; !ok {
+				diags[fileID] = []Diagnostic{}
+			}
+			diags[fileID] = append(diags[fileID], fileDiags...)
+		}
+	}
+
+	return compileResult{
+		lib:   libraryName,
+		diags: diags,
+	}, nil
+}
+
+// Read in the JSON IR at absolute filepath `jsonPath`.
+func (a *Analyzer) importLibrary(jsonPath string) error {
+	data, err := ioutil.ReadFile(jsonPath)
+	if err != nil {
+		return err
+	}
+	var lib FidlLibrary
+	if err := json.Unmarshal(data, &lib); err != nil {
+		return err
+	}
+	// TODO: import more things? files + deps?
+	libName := fidlcommon.MustReadLibraryName(lib.Name)
+	if _, ok := a.libs[libName]; !ok {
+		a.libs[libName] = &Library{}
+	}
+	a.libs[libName].ir = &lib
+	a.libs[libName].json = jsonPath
+
+	// Construct symbol map from JSON IR for the file's library
+	symbols, err := a.genSymbolMap(lib)
+	if err != nil {
+		return fmt.Errorf("unable to generate symbol map from library %s: %s", lib.Name, err)
+	}
+	a.libs[libName].symbols = symbols
+
+	return nil
+}
+
+type symbolMap map[string]*symbolInfo
+
+func (a *Analyzer) genSymbolMap(l FidlLibrary) (symbolMap, error) {
+	sm := make(symbolMap)
+
+	for _, d := range l.BitsDecls {
+		sm[d.Name] = &symbolInfo{
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				FromTypeAlias: d.FromTypeAlias.TypeAlias(),
+				Kind:          IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   BitsKind,
+					Bits: &BitsTypeInfo{
+						Type: d.Type.Type(),
+					},
+				},
+				Attrs: d.Attrs,
+			},
+		}
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
+	}
+	for _, d := range l.ConstDecls {
+		sm[d.Name] = &symbolInfo{
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				FromTypeAlias: d.FromTypeAlias.TypeAlias(),
+				Kind:          IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   ConstKind,
+					Const: &ConstTypeInfo{
+						Type:  d.Type.Type(),
+						Value: d.Value.Value,
+					},
+				},
+				Attrs: d.Attrs,
+			},
+		}
+	}
+	for _, d := range l.EnumDecls {
+		sm[d.Name] = &symbolInfo{
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				FromTypeAlias: d.FromTypeAlias.TypeAlias(),
+				Kind:          IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   EnumKind,
+					Enum: &EnumTypeInfo{
+						Type: PrimitiveSubtype(d.Type),
+					},
+				},
+				Attrs: d.Attrs,
+			},
+		}
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
+	}
+	for _, d := range l.ProtocolDecls {
+		sm[d.Name] = &symbolInfo{
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   ProtocolKind,
+				},
+				Attrs: d.Attrs,
+			},
+		}
+		for _, m := range d.Methods {
+			methodName := fmt.Sprintf("%s.%s", d.Name, m.Name)
+			sm[methodName] = &symbolInfo{
+				name:       m.Name,
+				definition: a.fidlLocToStateLoc(m.Loc),
+				typeInfo: Type{
+					IsMethod: true,
+					Attrs:    m.Attrs,
+				},
+			}
+			if len(m.MaybeRequest) > 0 {
+				a.addMembersToSymbolMap(sm, methodName, m.MaybeRequest)
+			}
+			if len(m.MaybeResponse) > 0 {
+				a.addMembersToSymbolMap(sm, methodName, m.MaybeResponse)
+			}
+		}
+	}
+	for _, d := range l.ServiceDecls {
+		sm[d.Name] = &symbolInfo{
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   ServiceKind,
+				},
+				Attrs: d.Attrs,
+			},
+		}
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
+	}
+	for _, d := range l.StructDecls {
+		// Skip anonymous structs, since we add them to the symbol map as method
+		// parameter structs
+		if d.Anonymous {
+			continue
+		}
+		sm[d.Name] = &symbolInfo{
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   StructKind,
+				},
+				Attrs: d.Attrs,
+			},
+		}
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
+	}
+	for _, d := range l.TableDecls {
+		sm[d.Name] = &symbolInfo{
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   TableKind,
+				},
+				Attrs: d.Attrs,
+			},
+		}
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
+	}
+	for _, d := range l.UnionDecls {
+		sm[d.Name] = &symbolInfo{
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   UnionKind,
+				},
+				Attrs: d.Attrs,
+			},
+		}
+		a.addMembersToSymbolMap(sm, d.Name, d.Members)
+	}
+	for _, d := range l.TypeAliasDecls {
+		sm[d.Name] = &symbolInfo{
+			name:       d.Name,
+			definition: a.fidlLocToStateLoc(d.Loc),
+			typeInfo: Type{
+				Kind: IdentifierType,
+				Identifier: &IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   d.Name,
+					Kind:   TypeAliasKind,
+					TypeAlias: &TypeAliasTypeInfo{
+						Type: d.TypeCtor.Type(),
+					},
+				},
+				Attrs: d.Attrs,
+			},
+		}
+	}
+
+	return sm, nil
+}
+
+func (a *Analyzer) addMembersToSymbolMap(sm symbolMap, declName string, members []member) {
+	for _, m := range members {
+		memberName := fmt.Sprintf("%s.%s", declName, m.Name)
+		sm[memberName] = &symbolInfo{
+			name:       m.Name,
+			definition: a.fidlLocToStateLoc(m.Loc),
+			typeInfo:   m.Type.Type(),
+		}
+		sm[memberName].typeInfo.FromTypeAlias = m.FromTypeAlias.TypeAlias()
+		sm[memberName].typeInfo.Attrs = m.Attrs
+	}
+}
+
+func (a *Analyzer) fidlLocToStateLoc(loc location) state.Location {
+	// If this JSON IR was compiled by the language server, the `Filename`s for
+	// for all the symbols will be tmp input files from the Analyzer. We want
+	// to point at the corresponding editor files for these tmp files.
+	fileID, err := a.inputFileToFileID(loc.Filename)
+	if err != nil {
+		// We assume `Filename` is a filepath to a precompiled JSON IR, provided
+		// in the CompiledLibraries on startup.
+		//
+		// The `Filename` in a decl's location is recorded relative to where
+		// fidlc was invoked. All the locations in CompiledLibraries are
+		// prepended with "../../" which can be replaced by the `BuildRootDir`.
+		fileID = state.FileID(strings.Replace(loc.Filename, "../..", a.cfg.BuildRootDir, 1))
+	}
+	return state.Location{
+		FileID: fileID,
+		Range: state.Range{
+			Start: state.Position{Line: loc.Line - 1, Character: loc.Column - 1},
+			End:   state.Position{Line: loc.Line - 1, Character: loc.Column - 1 + loc.Length},
+		},
+	}
+}
+
+func (d declType) Type() Type {
+	t := Type{Kind: d.Kind}
+
+	switch d.Kind {
+	default:
+		// Sometimes this will be called with a zeroed declType, e.g. for bits
+		// and enum members, which don't have types.
+		return t
+	case ArrayType:
+		t.Array = &ArrayTypeInfo{
+			ElementType:  d.ElementType.Type(),
+			ElementCount: d.ElementCount,
+		}
+	case VectorType:
+		t.Vector = &VectorTypeInfo{
+			ElementType:  d.ElementType.Type(),
+			ElementCount: d.MaybeElementCount,
+			Nullable:     d.Nullable,
+		}
+	case StringType:
+		t.String = &StringTypeInfo{
+			ElementCount: d.MaybeElementCount,
+			Nullable:     d.Nullable,
+		}
+	case HandleType:
+		t.Handle = &HandleTypeInfo{
+			Subtype:  HandleSubtype(d.Subtype),
+			Rights:   d.Rights,
+			Nullable: d.Nullable,
+		}
+	case RequestType:
+		t.Request = &RequestTypeInfo{
+			Subtype:  d.Subtype,
+			Nullable: d.Nullable,
+		}
+	case PrimitiveType:
+		t.Primitive = &PrimitiveTypeInfo{
+			Subtype: PrimitiveSubtype(d.Subtype),
+		}
+	case IdentifierType:
+		t.Identifier = &IdentifierTypeInfo{
+			Identifier: d.Identifier,
+			Nullable:   d.Nullable,
+		}
+	}
+
+	return t
+}
+
+func (t typeCtor) Type() Type {
+	switch t.Name {
+	case string(ArrayType):
+		ty := Type{
+			Kind:  ArrayType,
+			Array: &ArrayTypeInfo{},
+		}
+		if len(t.Args) > 0 {
+			ty.Array.ElementType = t.Args[0].Type()
+		}
+		if t.Size != nil {
+			if count, err := strconv.ParseUint(t.Size.Value, 10, 32); err == nil {
+				ty.Array.ElementCount = uint(count)
+			}
+		}
+		return ty
+
+	case string(VectorType):
+		ty := Type{
+			Kind: VectorType,
+			Vector: &VectorTypeInfo{
+				Nullable: t.Nullable,
+			},
+		}
+		if len(t.Args) > 0 {
+			ty.Vector.ElementType = t.Args[0].Type()
+		}
+		if t.Size != nil {
+			if count, err := strconv.ParseUint(t.Size.Value, 10, 32); err == nil {
+				countUint := uint(count)
+				ty.Vector.ElementCount = &countUint
+			}
+		}
+		return ty
+
+	case string(StringType):
+		ty := Type{
+			Kind: StringType,
+			String: &StringTypeInfo{
+				Nullable: t.Nullable,
+			},
+		}
+		if t.Size != nil {
+			if count, err := strconv.ParseUint(t.Size.Value, 10, 32); err == nil {
+				countUint := uint(count)
+				ty.String.ElementCount = &countUint
+			}
+		}
+		return ty
+
+	case string(HandleType):
+		ty := Type{
+			Kind: HandleType,
+			Handle: &HandleTypeInfo{
+				Nullable: t.Nullable,
+			},
+		}
+		if t.HandleSubtype != nil {
+			ty.Handle.Subtype = *t.HandleSubtype
+		}
+		return ty
+
+	case string(RequestType):
+		ty := Type{
+			Kind: RequestType,
+			Request: &RequestTypeInfo{
+				Nullable: t.Nullable,
+			},
+		}
+		if len(t.Args) > 0 {
+			ty.Request.Subtype = t.Args[0].Name
+		}
+		return ty
+
+	case string(Bool), Int8, Int16, Int32, Int64, Uint8, Uint16, Uint32, Uint64, Float32, Float64:
+		return Type{
+			Kind: PrimitiveType,
+			Primitive: &PrimitiveTypeInfo{
+				Subtype: PrimitiveSubtype(t.Name),
+			},
+		}
+
+	default:
+		// We assume it is the name of an identifier type.
+		return Type{
+			Kind: IdentifierType,
+			Identifier: &IdentifierTypeInfo{
+				Identifier: t.Name,
+				Nullable:   t.Nullable,
+			},
+		}
+	}
+}
+
+func (t typeCtor) TypeAlias() *string {
+	if t.Name == "" {
+		return nil
+	}
+	return &t.Name
+}
diff --git a/fidl-lsp/analysis/definition.go b/fidl-lsp/analysis/definition.go
new file mode 100644
index 0000000..566b3be
--- /dev/null
+++ b/fidl-lsp/analysis/definition.go
@@ -0,0 +1,102 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+// DefinitionOfSymbol returns the location (or locations) where the given symbol
+// is defined, as long as that location is available to the Analyzer (i.e. in
+// a.libs somewhere).
+//
+// For a library name, e.g. `fuchsia.foo`, DefinitionOfSymbol returns the
+// locations of the `library` declarations for each file in that library.
+//
+// DefinitionOfSymbol only works for library names and top-level names: enums,
+// bits, structs, protocols, etc. -- not for member names. For example, it can
+// find the definition of `Foo` in the following example:
+//
+// 		protocol P {
+// 			Method(Foo foo);
+// 		};
+//
+// But it can't find the definition of `B.FOO`:
+//
+// 		bits B {
+//			FOO = 0;
+// 		}
+//		const ZERO = B.FOO;
+//
+func (a *Analyzer) DefinitionOfSymbol(fs *state.FileSystem, sym state.Symbol) ([]state.Location, error) {
+	// If `sym` is a library name, return locations pointing at all the files in
+	// the library (specifically, pointing at their `library` declarations).
+	libName, err := fidlcommon.ReadLibraryName(sym.Name)
+	if err == nil {
+		if _, isLib := a.libs[libName]; isLib {
+			return a.declarationsOfLibrary(fs, libName), nil
+		}
+	}
+
+	// Otherwise, we assume it is a local or fully-qualified name
+	name, err := a.symbolToFullyQualifiedName(fs, sym)
+	if err != nil {
+		return nil, fmt.Errorf(
+			"could not convert symbol `%s` to fully-qualified name: %s",
+			sym.Name,
+			err,
+		)
+	}
+
+	symInfo, err := a.lookupSymbolInfo(name)
+	if err != nil {
+		return nil, fmt.Errorf("could not find definition of symbol `%s`: %s", name.FullyQualifiedName(), err)
+	}
+
+	return []state.Location{symInfo.definition}, nil
+}
+
+func (a *Analyzer) declarationsOfLibrary(fs *state.FileSystem, lib fidlcommon.LibraryName) []state.Location {
+	files := []state.FileID{}
+	for file := range a.libs[lib].files {
+		// These files are all absolute paths, but some are to temporary input
+		// files used by the Analyzer. For these, we want to convert the path
+		// to the document URI the editor knows about.
+		if fileID, err := a.inputFileToFileID(file); err != nil {
+			files = append(files, state.FileID(file))
+		} else {
+			files = append(files, fileID)
+		}
+	}
+
+	locs := []state.Location{}
+	// Find the `library` declaration in each file
+	for _, fileID := range files {
+		file, err := fs.File(fileID)
+		if err != nil {
+			// If we don't have the file in memory, read it in
+			bytes, err := ioutil.ReadFile(string(fileID))
+			if err != nil {
+				log.Printf("could not read in file `%s`: %s", fileID, err)
+				continue
+			}
+			file = string(bytes)
+		}
+
+		if libraryMatch, ok := state.ParseLibraryMatch(file); ok {
+			locs = append(locs, state.Location{
+				FileID: fileID,
+				Range:  libraryMatch.Range,
+			})
+		}
+	}
+	return locs
+}
diff --git a/fidl-lsp/analysis/definition_test.go b/fidl-lsp/analysis/definition_test.go
new file mode 100644
index 0000000..3bd4c2e
--- /dev/null
+++ b/fidl-lsp/analysis/definition_test.go
@@ -0,0 +1,383 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis_test
+
+import (
+	"os"
+	"testing"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/state"
+)
+
+var (
+	fuchsiaDir     = os.Getenv("FUCHSIA_DIR")
+	toolsPath      = fuchsiaDir + "/out/default.zircon/tools/"
+	fidlcPath      = toolsPath + "fidlc"
+	fidlLintPath   = toolsPath + "fidl-lint"
+	fidlFormatPath = toolsPath + "fidl-format"
+)
+
+func defaultConfig() analysis.Config {
+	return analysis.Config{
+		BuildRootDir:   fuchsiaDir,
+		FidlcPath:      fidlcPath,
+		FidlLintPath:   fidlLintPath,
+		FidlFormatPath: fidlFormatPath,
+	}
+}
+
+func TestDefinitionOfLibrary(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"file.fidl",
+		`library test;`,
+	)
+	if err := analyzer.Analyze(fs, "file.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+
+	locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+		Name: "test",
+		Location: state.Location{
+			FileID: state.FileID("file.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 0, Character: 8},
+				End:   state.Position{Line: 0, Character: 12},
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("error getting definition of symbol: %s", err)
+	}
+
+	expLoc := state.Location{
+		FileID: state.FileID("file.fidl"),
+		Range: state.Range{
+			Start: state.Position{Line: 0, Character: 8},
+			End:   state.Position{Line: 0, Character: 12},
+		},
+	}
+	if len(locs) != 1 {
+		t.Fatalf("expected 1 definition location")
+	}
+	if locs[0] != expLoc {
+		t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+	}
+}
+
+func TestDefinitionOfLibraryMultipleFiles(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"file1.fidl",
+		`library test;`,
+	)
+	fs.NewFile(
+		"file2.fidl",
+		`library test;`,
+	)
+	if err := analyzer.Analyze(fs, "file1.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+	if err := analyzer.Analyze(fs, "file2.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+
+	locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+		Name: "test",
+		Location: state.Location{
+			FileID: state.FileID("file1.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 0, Character: 8},
+				End:   state.Position{Line: 0, Character: 12},
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("error getting definition of symbol: %s", err)
+	}
+
+	expLocs := []state.Location{
+		{
+			FileID: state.FileID("file1.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 0, Character: 8},
+				End:   state.Position{Line: 0, Character: 12},
+			},
+		},
+		{
+			FileID: state.FileID("file2.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 0, Character: 8},
+				End:   state.Position{Line: 0, Character: 12},
+			},
+		},
+	}
+	if len(locs) != len(expLocs) {
+		t.Fatalf("expected %d definition locations", len(expLocs))
+	}
+	for _, expLoc := range expLocs {
+		found := false
+		for _, loc := range locs {
+			if expLoc == loc {
+				found = true
+				break
+			}
+		}
+		if !found {
+			t.Errorf("expected but did not find definition of symbol: %v", expLoc)
+		}
+	}
+}
+
+func TestDefinitionOfLibraryImport(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"import.fidl",
+		`
+library fuchsia.import;
+
+const uint8 FOO = 0;
+`,
+	)
+	fs.NewFile(
+		"test.fidl",
+		`
+library test;
+using fuchsia.import;
+
+const uint8 BAR = fuchsia.import.FOO;
+`,
+	)
+	if err := analyzer.Analyze(fs, "import.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+	if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+
+	locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+		Name: "fuchsia.import",
+		Location: state.Location{
+			FileID: state.FileID("test.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 3, Character: 6},
+				End:   state.Position{Line: 3, Character: 20},
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("error getting definition of symbol: %s", err)
+	}
+
+	expLoc := state.Location{
+		FileID: state.FileID("import.fidl"),
+		Range: state.Range{
+			Start: state.Position{Line: 1, Character: 8},
+			End:   state.Position{Line: 1, Character: 22},
+		},
+	}
+	if len(locs) != 1 {
+		t.Fatalf("expected 1 definition location")
+	}
+	if locs[0] != expLoc {
+		t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+	}
+}
+
+func TestDefinitionOfFullyQualifiedSymbol(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"import.fidl",
+		`
+library fuchsia.import;
+
+struct Foo {};
+`,
+	)
+	fs.NewFile(
+		"test.fidl",
+		`
+library test;
+using fuchsia.import;
+
+protocol Baz {
+	Method(fuchsia.import.Foo f);
+};
+`,
+	)
+	if err := analyzer.Analyze(fs, "import.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+	if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+
+	locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+		Name: "fuchsia.import.Foo",
+		Location: state.Location{
+			FileID: state.FileID("test.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 5, Character: 11},
+				End:   state.Position{Line: 5, Character: 29},
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("error getting definition of symbol: %s", err)
+	}
+
+	expLoc := state.Location{
+		FileID: state.FileID("import.fidl"),
+		Range: state.Range{
+			Start: state.Position{Line: 3, Character: 7},
+			End:   state.Position{Line: 3, Character: 10},
+		},
+	}
+	if len(locs) != 1 {
+		t.Fatalf("expected 1 definition location")
+	}
+	if locs[0] != expLoc {
+		t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+	}
+}
+
+func TestDefinitionOfLocalSymbol(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"test.fidl",
+		`
+library test;
+
+struct Foo {};
+
+protocol Baz {
+	Method(Foo f);
+};
+`,
+	)
+	if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+
+	locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+		Name: "Foo",
+		Location: state.Location{
+			FileID: state.FileID("test.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 6, Character: 11},
+				End:   state.Position{Line: 6, Character: 14},
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("error getting definition of symbol: %s", err)
+	}
+
+	expLoc := state.Location{
+		FileID: state.FileID("test.fidl"),
+		Range: state.Range{
+			Start: state.Position{Line: 3, Character: 7},
+			End:   state.Position{Line: 3, Character: 10},
+		},
+	}
+	if len(locs) != 1 {
+		t.Fatalf("expected 1 definition location")
+	}
+	if locs[0] != expLoc {
+		t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+	}
+}
+
+func TestDefinitionOfSymbolInSameLibraryDifferentFile(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"test1.fidl",
+		`
+library test;
+
+struct Foo {};
+`,
+	)
+	fs.NewFile(
+		"test2.fidl",
+		`
+library test;
+
+protocol Baz {
+	Method(Foo f);
+};
+`,
+	)
+	if err := analyzer.Analyze(fs, "test1.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+	if err := analyzer.Analyze(fs, "test2.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+
+	locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+		Name: "Foo",
+		Location: state.Location{
+			FileID: state.FileID("test2.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 5, Character: 11},
+				End:   state.Position{Line: 5, Character: 14},
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("error getting definition of symbol: %s", err)
+	}
+
+	expLoc := state.Location{
+		FileID: state.FileID("test1.fidl"),
+		Range: state.Range{
+			Start: state.Position{Line: 3, Character: 7},
+			End:   state.Position{Line: 3, Character: 10},
+		},
+	}
+	if len(locs) != 1 {
+		t.Fatalf("expected 1 definition location")
+	}
+	if locs[0] != expLoc {
+		t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+	}
+}
+
+func TestDefinitionOfReservedKeyword(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"test.fidl",
+		`
+library test;
+
+struct Foo {};
+`,
+	)
+	if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+		t.Fatalf("failed to analyze file: %s", err)
+	}
+
+	if _, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+		Name: "struct",
+		Location: state.Location{
+			FileID: state.FileID("test.fidl"),
+			Range: state.Range{
+				Start: state.Position{Line: 3, Character: 0},
+				End:   state.Position{Line: 3, Character: 6},
+			},
+		},
+	}); err == nil {
+		t.Fatalf("expect error on definition of reserved keyword")
+	}
+}
diff --git a/fidl-lsp/analysis/deps.go b/fidl-lsp/analysis/deps.go
new file mode 100644
index 0000000..506ecfa
--- /dev/null
+++ b/fidl-lsp/analysis/deps.go
@@ -0,0 +1,190 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+// CompiledLibraries represents a deserialized fidl_project.json.
+// This is a file that declares all the FIDL libraries the language server
+// should be aware of, and includes information on how to build them and find
+// their compiled JSON IR.
+type CompiledLibraries map[string]CompiledLibrary
+
+// CompiledLibrary represents the build information for a single FIDL library,
+// including its constituent files, its dependencies, and the absolute filepath
+// to its compiled JSON IR.
+type CompiledLibrary struct {
+	Files []string
+	Deps  []string
+	JSON  string
+}
+
+// LoadFidlProject builds a CompiledLibraries out of a fidl_project.json file.
+func LoadFidlProject(fidlProjectPath string) (CompiledLibraries, error) {
+	data, err := ioutil.ReadFile(fidlProjectPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read file: %s", err)
+	}
+
+	var libraries CompiledLibraries
+	if err := json.Unmarshal(data, &libraries); err != nil {
+		return nil, fmt.Errorf("failed to deserialize compiled libraries: %s", err)
+	}
+	return libraries, nil
+}
+
+// A libraryMap stores information derived about a group of libraries.
+type libraryMap map[fidlcommon.LibraryName]libraryInfo
+
+type libraryInfo struct {
+	// FIDL files that make up the library (relative to dir, or absolute).
+	files []string
+	// Direct dependencies (imported by "using" declarations in files).
+	deps []fidlcommon.LibraryName
+}
+
+// empty returns true if i represents an empty library (has no files).
+func (i *libraryInfo) empty() bool {
+	return len(i.files) == 0
+}
+
+// FindDeps analyzes the file at `path`, locates its dependencies, and returns
+// the --files args for a fidlc invocation that will compile `file`'s library.
+func (a *Analyzer) FindDeps(fs *state.FileSystem, path state.FileID) ([]string, error) {
+	result := make(libraryMap)
+
+	// Start with a stack containing the root library (if it's a platform source
+	// library, meaning we can expect to find it in the tree) and its imports.
+	var stack []fidlcommon.LibraryName
+	file, err := fs.File(path)
+	if err != nil {
+		return nil, fmt.Errorf("could not find file `%s`", path)
+	}
+	rootLibraryMatch, rootLibraryOk := state.ParseLibraryMatch(file)
+	if !rootLibraryOk {
+		return nil, fmt.Errorf("no library found for file %s", file)
+	}
+	stack = append(stack, fidlcommon.LibraryName(rootLibraryMatch.Lib))
+	rootImports := state.ParsePlatformImportsMatch(file)
+	for _, m := range rootImports {
+		stack = append(stack, fidlcommon.LibraryName(m.Lib))
+	}
+
+	// Explore all dependencies via depth-first search.
+	for len(stack) != 0 {
+		// Pop a library and check if it needs processing.
+		var libName fidlcommon.LibraryName
+		libName, stack = stack[len(stack)-1], stack[:len(stack)-1]
+		if _, ok := result[libName]; ok {
+			continue
+		}
+
+		// Find the library's files and dependencies.
+		var info libraryInfo
+		if lib, ok := a.libs[libName]; ok {
+			// Add the library's files to info.files
+			for file := range lib.files {
+				// Unless that file is a duplicate of one that is open
+				if file == string(path) {
+					continue
+				}
+				info.files = append(info.files, file)
+			}
+			// Add each of the library's dependencies to the stack
+			for dep := range lib.deps {
+				info.deps = append(info.deps, dep)
+				stack = append(stack, dep)
+			}
+		} else {
+			return nil, fmt.Errorf("could not find deps for library `%s`", libName)
+		}
+
+		if !info.empty() {
+			result[libName] = info
+		}
+	}
+
+	// `result` is now a libraryMap of the dependencies for the current file
+
+	// Now do a topological sort on libraryMap (this will give us a list of
+	// files in order of dependency for each library) and reverse order this
+	// to get the correct list of --files args to return
+	fileArgs, err := result.fidlcFileArgs()
+	if err != nil {
+		return nil, fmt.Errorf("error in getting fidlc --files args: %s", err)
+	}
+	return fileArgs, nil
+}
+
+// fidlcFileArgs returns a list of arguments to pass to fidlc to compile the
+// libraries in m. This is a fairly expensive operation. It returns an error if
+// there is an import cycle.
+func (m libraryMap) fidlcFileArgs() ([]string, error) {
+	sorted, err := m.topologicalSort()
+	if err != nil {
+		return nil, err
+	}
+
+	// Iterate in reverse order so that dependencies come before dependents.
+	var args []string
+	for i := len(sorted) - 1; i >= 0; i-- {
+		lib := sorted[i]
+		args = append(args, "--files")
+		info := m[lib]
+		for _, file := range info.files {
+			args = append(args, file)
+		}
+	}
+	return args, nil
+}
+
+// topologicalSort runs topological sort on m, whose deps fields define a
+// directed graph in adjacency list form. In the resulting list, libraries only
+// depend on other libraries later in the list, not on earlier ones. Returns
+// an error if there is a cycle.
+func (m libraryMap) topologicalSort() ([]fidlcommon.LibraryName, error) {
+	var sorted []fidlcommon.LibraryName
+	incoming := map[fidlcommon.LibraryName]int{}
+	for u := range m {
+		incoming[u] = 0
+	}
+	for _, info := range m {
+		for _, v := range info.deps {
+			incoming[v]++
+		}
+	}
+	var src []fidlcommon.LibraryName
+	for u, deg := range incoming {
+		if deg == 0 {
+			src = append(src, u)
+		}
+	}
+	for len(src) != 0 {
+		var u fidlcommon.LibraryName
+		u, src = src[len(src)-1], src[:len(src)-1]
+		sorted = append(sorted, u)
+		for _, v := range m[u].deps {
+			incoming[v]--
+			if incoming[v] == 0 {
+				src = append(src, v)
+			}
+		}
+	}
+	for _, deg := range incoming {
+		if deg != 0 {
+			return nil, errors.New("detected an import cycle")
+		}
+	}
+	return sorted, nil
+}
diff --git a/fidl-lsp/analysis/deps_test.go b/fidl-lsp/analysis/deps_test.go
new file mode 100644
index 0000000..04e7140
--- /dev/null
+++ b/fidl-lsp/analysis/deps_test.go
@@ -0,0 +1,274 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis_test
+
+import (
+	"testing"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/state"
+)
+
+func indexOf(slice []string, elem string) int {
+	for i, v := range slice {
+		if v == elem {
+			return i
+		}
+	}
+	return -1
+}
+
+func TestFindDepsNoDeps(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(analysis.Config{}, analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"file.fidl",
+		`library test;`,
+	)
+
+	analyzer.Analyze(fs, "file.fidl")
+	fidlcArgs, err := analyzer.FindDeps(fs, "file.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	if len(fidlcArgs) != 2 {
+		t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), 2)
+	}
+}
+
+func TestFindDepsDirectImports(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(
+		analysis.Config{},
+		analysis.CompiledLibraries{
+			"fuchsia.import1": analysis.CompiledLibrary{
+				Files: []string{"import1.fidl"},
+			},
+			"fuchsia.import2": analysis.CompiledLibrary{
+				Files: []string{"import2.fidl"},
+			},
+		},
+	)
+
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"test.fidl",
+		`
+library test;
+using fuchsia.import1;
+using fuchsia.import2;
+`,
+	)
+
+	analyzer.Analyze(fs, "test.fidl")
+	fidlcArgs, err := analyzer.FindDeps(fs, "test.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	expArgs := []string{
+		"--files", "import1.fidl",
+		"--files", "import2.fidl",
+		"--files",
+	}
+	// FindDeps returns the required --files args to fidlc, including the library
+	// itself, which we don't check. So there should be (# of dependencies + 1)
+	// total --files args.
+	if len(fidlcArgs) != len(expArgs)+1 {
+		t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+fs.NFiles())
+	}
+	for _, expArg := range expArgs {
+		if indexOf(fidlcArgs, expArg) == -1 {
+			t.Errorf("missing expected fidlc --files arg %s", expArg)
+		}
+	}
+}
+
+func TestFindDepsIndirectImports(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(
+		analysis.Config{},
+		analysis.CompiledLibraries{
+			"fuchsia.import1": analysis.CompiledLibrary{
+				Files: []string{"import1.fidl"},
+				Deps:  []string{"fuchsia.import2", "fuchsia.import3"},
+			},
+			"fuchsia.import2": analysis.CompiledLibrary{
+				Files: []string{"import2.fidl"},
+				Deps:  []string{"fuchsia.import4"},
+			},
+			"fuchsia.import3": analysis.CompiledLibrary{
+				Files: []string{"import3.fidl"},
+			},
+			"fuchsia.import4": analysis.CompiledLibrary{
+				Files: []string{"import4.fidl"},
+			},
+		},
+	)
+
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"test.fidl",
+		`
+library test;
+using fuchsia.import1; // depends on import2, import3
+using fuchsia.import2; // depends on import 4
+`,
+	)
+
+	analyzer.Analyze(fs, "test.fidl")
+	fidlcArgs, err := analyzer.FindDeps(fs, "test.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	expArgs := []string{
+		"--files", "import4.fidl",
+		"--files", "import3.fidl",
+		"--files", "import2.fidl",
+		"--files", "import1.fidl",
+		"--files",
+	}
+	if len(fidlcArgs) != len(expArgs)+1 {
+		t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+fs.NFiles())
+	}
+	for _, expArg := range expArgs {
+		if indexOf(fidlcArgs, expArg) == -1 {
+			t.Errorf("missing expected fidlc --files arg %s", expArg)
+		}
+	}
+	if indexOf(fidlcArgs, "import4.fidl") > indexOf(fidlcArgs, "import2.fidl") ||
+		indexOf(fidlcArgs, "import3.fidl") > indexOf(fidlcArgs, "import1.fidl") ||
+		indexOf(fidlcArgs, "import2.fidl") > indexOf(fidlcArgs, "import1.fidl") {
+		t.Errorf("fidlc --files are not in correct dependency order")
+	}
+}
+
+func TestFindDepsMultiFileLibraries(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(
+		analysis.Config{},
+		analysis.CompiledLibraries{
+			"fuchsia.import1": analysis.CompiledLibrary{
+				Files: []string{"import1_1.fidl", "import1_2.fidl"},
+				Deps:  []string{"fuchsia.import3", "fuchsia.import4"},
+			},
+			"fuchsia.import2": analysis.CompiledLibrary{
+				Files: []string{"import2_1.fidl", "import2_2.fidl"},
+				Deps:  []string{"fuchsia.import4"},
+			},
+			"fuchsia.import3": analysis.CompiledLibrary{
+				Files: []string{"import3.fidl"},
+			},
+			"fuchsia.import4": analysis.CompiledLibrary{
+				Files: []string{"import4_1.fidl", "import4_2.fidl", "import4_3.fidl"},
+			},
+		},
+	)
+
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"test1.fidl",
+		`
+library test;
+using fuchsia.import1; // depends on import2, import3
+`,
+	)
+	fs.NewFile(
+		"test2.fidl",
+		`
+library test;
+using fuchsia.import2; // depends on import 4
+`,
+	)
+
+	analyzer.Analyze(fs, "test1.fidl")
+	analyzer.Analyze(fs, "test2.fidl")
+	fidlcArgs, err := analyzer.FindDeps(fs, "test1.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	expArgs := []string{
+		"--files", "import4_1.fidl", "import4_2.fidl", "import4_3.fidl",
+		"--files", "import3.fidl",
+		"--files", "import2_1.fidl", "import2_2.fidl",
+		"--files", "import1_1.fidl", "import1_2.fidl",
+		"--files",
+	}
+	if len(fidlcArgs) != len(expArgs)+2 {
+		t.Errorf("args: %v\n", fidlcArgs)
+		t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+fs.NFiles())
+	}
+	// We don't test the topological sort, just that all the required files are present.
+	for _, expArg := range expArgs {
+		if indexOf(fidlcArgs, expArg) == -1 {
+			t.Errorf("missing expected fidlc --files arg %s", expArg)
+		}
+	}
+}
+
+func TestFindDepsNoLibraryInFile(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(analysis.Config{}, analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"test1.fidl",
+		`const FOO = 0;`,
+	)
+
+	analyzer.Analyze(fs, "test1.fidl")
+	if _, err := analyzer.FindDeps(fs, "test1.fidl"); err == nil {
+		t.Errorf("expect error on file with no library")
+	}
+}
+
+func TestFindDepsImportNotInFidlProject(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(analysis.Config{}, analysis.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"foo.fidl",
+		`
+library foo;
+using fuchsia.bar;
+`,
+	)
+
+	analyzer.Analyze(fs, "foo.fidl")
+	if _, err := analyzer.FindDeps(fs, "foo.fidl"); err == nil {
+		t.Errorf("expect error on unknown import")
+	}
+}
+
+func TestFindDepsImportCycle(t *testing.T) {
+	analyzer := analysis.NewAnalyzer(
+		analysis.Config{},
+		analysis.CompiledLibraries{
+			"fuchsia.foo": analysis.CompiledLibrary{
+				Files: []string{"foo.fidl"},
+				Deps:  []string{"fuchsia.bar"},
+			},
+			"fuchsia.bar": analysis.CompiledLibrary{
+				Files: []string{"bar.fidl"},
+				Deps:  []string{"fuchsia.foo"},
+			},
+		},
+	)
+
+	fs := state.NewFileSystem()
+	fs.NewFile(
+		"foo.fidl",
+		`
+library fuchsia.foo;
+using fuchsia.bar;
+`,
+	)
+	fs.NewFile(
+		"bar.fidl",
+		`
+library fuchsia.bar;
+using fuchsia.foo;
+`,
+	)
+
+	analyzer.Analyze(fs, "foo.fidl")
+	analyzer.Analyze(fs, "bar.fidl")
+	if _, err := analyzer.FindDeps(fs, "foo.fidl"); err == nil {
+		t.Errorf("expect error on import cycle")
+	}
+}
diff --git a/fidl-lsp/analysis/diagnostics.go b/fidl-lsp/analysis/diagnostics.go
new file mode 100644
index 0000000..05a42b6
--- /dev/null
+++ b/fidl-lsp/analysis/diagnostics.go
@@ -0,0 +1,95 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"encoding/json"
+	"fmt"
+	"os/exec"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+// DiagnosticsOnLibrary retrieves the cached diagnostics for `lib`.
+func (a *Analyzer) DiagnosticsOnLibrary(lib fidlcommon.LibraryName) (map[state.FileID][]Diagnostic, error) {
+	if _, ok := a.libs[lib]; !ok {
+		return nil, fmt.Errorf("could not find library `%s`", lib)
+	}
+	return a.libs[lib].diags, nil
+}
+
+type fidlLintSuggestion struct {
+	Description string
+}
+
+type FidlcTool string
+
+const (
+	Fidlc    FidlcTool = "fidlc"
+	FidlLint           = "fidl-lint"
+)
+
+// Diagnostic represents an {error, warning, lint} deserialized from the JSON
+// output of fidlc or fidl-lint.
+type Diagnostic struct {
+	Source      FidlcTool
+	Category    string
+	Message     string
+	Path        string
+	StartLine   int `json:"start_line"`
+	StartChar   int `json:"start_char"`
+	EndLine     int `json:"end_line"`
+	EndChar     int `json:"end_char"`
+	Suggestions []fidlLintSuggestion
+}
+
+func (a *Analyzer) fidlcDiagsFromStderr(stderr []byte) (map[state.FileID][]Diagnostic, error) {
+	// Deserialize stderr into []Diagnostic
+	var diags []Diagnostic
+	if err := json.Unmarshal(stderr, &diags); err != nil {
+		return nil, fmt.Errorf("could not deserialize fidlc diagnostics: `%s`", err)
+	}
+	fileToDiags := make(map[state.FileID][]Diagnostic)
+	for i := range diags {
+		fileID, err := a.inputFileToFileID(diags[i].Path)
+		if err != nil {
+			continue
+		}
+		if _, ok := fileToDiags[fileID]; !ok {
+			fileToDiags[fileID] = []Diagnostic{}
+		}
+		diags[i].Source = Fidlc
+		fileToDiags[fileID] = append(fileToDiags[fileID], diags[i])
+	}
+	return fileToDiags, nil
+}
+
+func (a *Analyzer) runFidlLint(path string) (map[state.FileID][]Diagnostic, error) {
+	// TODO: return early with an error if we got status code != 0 or 1?
+	// Otherwise, ignore err: we expect this to fail with status code 1 if there
+	// are any lints from fidl-lint.
+	out, _ := exec.Command(a.cfg.FidlLintPath, "--format=json", path).Output()
+
+	// Deserialize stdout into []fidlLint
+	var diags []Diagnostic
+	if err := json.Unmarshal(out, &diags); err != nil {
+		return nil, fmt.Errorf("error deserializing fidl-lint output: %s", err)
+	}
+	fileToDiags := make(map[state.FileID][]Diagnostic)
+	for i := range diags {
+		fileID, err := a.inputFileToFileID(diags[i].Path)
+		if err != nil {
+			continue
+		}
+		if _, ok := fileToDiags[fileID]; !ok {
+			fileToDiags[fileID] = []Diagnostic{}
+		}
+		diags[i].Source = FidlLint
+		fileToDiags[fileID] = append(fileToDiags[fileID], diags[i])
+	}
+	return fileToDiags, nil
+}
diff --git a/fidl-lsp/analysis/format.go b/fidl-lsp/analysis/format.go
new file mode 100644
index 0000000..8d09d0a
--- /dev/null
+++ b/fidl-lsp/analysis/format.go
@@ -0,0 +1,28 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"fmt"
+	"os/exec"
+
+	"fidl-lsp/state"
+)
+
+// FormatFile runs fidl-format on the file at `path` in the FileSystem, and
+// returns the formatted document, or an error.
+func (a *Analyzer) FormatFile(fs *state.FileSystem, path state.FileID) (string, error) {
+	// TODO: write tmp file on all changes in langserver?
+	analysisInputFile, err := a.writeFileToTmp(fs, path)
+	if err != nil {
+		return "", fmt.Errorf("error writing to tmp file: %s", err)
+	}
+
+	out, err := exec.Command(a.cfg.FidlFormatPath, analysisInputFile).Output()
+	if err != nil {
+		return "", fmt.Errorf("error running fidl-format: %s", err)
+	}
+	return string(out), nil
+}
diff --git a/fidl-lsp/analysis/library.go b/fidl-lsp/analysis/library.go
new file mode 100644
index 0000000..1efa0f8
--- /dev/null
+++ b/fidl-lsp/analysis/library.go
@@ -0,0 +1,173 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+// Map from fully qualified name to type.kind, e.g.
+// "fuchsia.hardware.camera/Device": "interface"
+type decls map[string]string
+
+type dependency struct {
+	Name         string
+	Declarations decls
+}
+
+type location struct {
+	Filename             string
+	Line, Column, Length int
+}
+
+type Attribute struct {
+	Name  string
+	Value string
+}
+
+type TypeKind string
+
+const (
+	ArrayType      TypeKind = "array"
+	VectorType              = "vector"
+	StringType              = "string"
+	HandleType              = "handle"
+	RequestType             = "request"
+	PrimitiveType           = "primitive"
+	IdentifierType          = "identifier"
+)
+
+type typeCtor struct {
+	Name          string
+	Args          []typeCtor
+	Nullable      bool
+	Size          *constValue    `json:"maybe_size,omitempty"`
+	HandleSubtype *HandleSubtype `json:"maybe_handle_subtype,omitempty"`
+}
+
+type constValue struct {
+	Value string
+}
+
+type declType struct {
+	Kind TypeKind
+
+	// Array
+	ElementType  *declType `json:"element_type,omitempty"`
+	ElementCount uint      `json:"element_count,omitempty"`
+
+	// Vector
+	MaybeElementCount *uint `json:"maybe_element_count,omitempty"`
+	Nullable          bool  `json:"nullable,omitempty"`
+
+	// Handle
+	Subtype string `json:"subtype,omitempty"`
+	Rights  uint   `json:"rights,omitempty"`
+
+	Identifier string `json:"identifier,omitempty"`
+}
+
+type member struct {
+	Name          string
+	Loc           location    `json:"location"`
+	Type          declType    `json:"type,omitempty"`
+	FromTypeAlias typeCtor    `json:"experimental_maybe_from_type_alias,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type method struct {
+	Name          string
+	Loc           location    `json:"location"`
+	MaybeRequest  []member    `json:"maybe_request,omitempty"`
+	MaybeResponse []member    `json:"maybe_response,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type bitsDecl struct {
+	Name          string
+	Loc           location `json:"location"`
+	Type          declType
+	FromTypeAlias typeCtor    `json:"experimental_maybe_from_type_alias,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
+	Members       []member
+}
+
+type enumDecl struct {
+	Name string
+	Loc  location `json:"location"`
+	// For some reason, Bits have a full type in the JSON IR, while enums have
+	// a string
+	Type          string
+	FromTypeAlias typeCtor    `json:"experimental_maybe_from_type_alias,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
+	Members       []member
+}
+
+type constDecl struct {
+	Name          string
+	Loc           location `json:"location"`
+	Type          declType
+	Value         constValue  `json:"value"`
+	FromTypeAlias typeCtor    `json:"experimental_maybe_from_type_alias,omitempty"`
+	Attrs         []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type protocolDecl struct {
+	Name    string
+	Loc     location `json:"location"`
+	Methods []method
+	Attrs   []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type serviceDecl struct {
+	Name    string
+	Loc     location `json:"location"`
+	Members []member
+	Attrs   []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type structDecl struct {
+	Name      string
+	Loc       location `json:"location"`
+	Members   []member
+	Attrs     []Attribute `json:"maybe_attributes,omitempty"`
+	Anonymous bool
+}
+
+type tableDecl struct {
+	Name    string
+	Loc     location `json:"location"`
+	Members []member
+	Attrs   []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type unionDecl struct {
+	Name    string
+	Loc     location `json:"location"`
+	Members []member
+	Attrs   []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+type typeAliasDecl struct {
+	Name     string
+	Loc      location    `json:"location"`
+	TypeCtor typeCtor    `json:"partial_type_ctor"`
+	Attrs    []Attribute `json:"maybe_attributes,omitempty"`
+}
+
+// TODO: rename to FidlJSONIR, or JSONLibrary?
+
+// FidlLibrary is the type of the deserialized JSON IR of a FIDL library.
+type FidlLibrary struct {
+	Version        string
+	Name           string
+	Deps           []dependency    `json:"library_dependencies"`
+	BitsDecls      []bitsDecl      `json:"bits_declarations"`
+	ConstDecls     []constDecl     `json:"const_declarations"`
+	EnumDecls      []enumDecl      `json:"enum_declarations"`
+	ProtocolDecls  []protocolDecl  `json:"interface_declarations"`
+	ServiceDecls   []serviceDecl   `json:"service_declarations"`
+	StructDecls    []structDecl    `json:"struct_declarations"`
+	TableDecls     []tableDecl     `json:"table_declarations"`
+	UnionDecls     []unionDecl     `json:"union_declarations"`
+	TypeAliasDecls []typeAliasDecl `json:"type_alias_declarations"`
+	Decls          decls           `json:"declarations"`
+}
diff --git a/fidl-lsp/analysis/references.go b/fidl-lsp/analysis/references.go
new file mode 100644
index 0000000..346f9c9
--- /dev/null
+++ b/fidl-lsp/analysis/references.go
@@ -0,0 +1,173 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+// ReferencesToSymbol returns the locations in `fs` of symbols that reference
+// `sym`, as long as those locations are available to the Analyzer (i.e. in
+// a.libs somewhere).
+//
+// For a library name, e.g. `fuchsia.foo`, ReferencesToSymbol returns the
+// locations of the `using` import declarations for each file that imports that
+// library.
+func (a *Analyzer) ReferencesToSymbol(fs *state.FileSystem, sym state.Symbol) ([]state.Location, error) {
+	// If `sym` is a library name, return locations pointing at all the files
+	// that import that library (specifically, pointing at their `using`
+	// declarations).
+	libName, err := fidlcommon.ReadLibraryName(sym.Name)
+	if err == nil {
+		if _, isLib := a.libs[libName]; isLib {
+			return a.importsOfLibrary(fs, libName), nil
+		}
+	}
+
+	// Otherwise, we assume it is a local or fully-qualified name
+	name, err := a.symbolToFullyQualifiedName(fs, sym)
+	if err != nil {
+		return nil, fmt.Errorf(
+			"could not convert symbol `%s` to fully-qualified name: %s",
+			sym.Name,
+			err,
+		)
+	}
+
+	// Search for references to `name` in all the libraries that import `name`'s
+	// library, as well as its own library.
+	libs := a.librariesThatImport(name.LibraryName())
+	libs = append(libs, name.LibraryName())
+	refs := []state.Location{}
+
+	for _, lib := range libs {
+		if a.libs[lib].ir == nil {
+			if err := a.importLibrary(a.libs[lib].json); err != nil {
+				return nil, fmt.Errorf(
+					"error importing library `%s`: %s",
+					name.LibraryName().FullyQualifiedName(),
+					err,
+				)
+			}
+		}
+
+		for _, si := range a.libs[lib].symbols {
+			if a.typeReferencesName(si.typeInfo, name) {
+				refs = append(refs, si.definition)
+			}
+		}
+	}
+
+	return refs, nil
+}
+
+func (a *Analyzer) importsOfLibrary(fs *state.FileSystem, importedLib fidlcommon.LibraryName) []state.Location {
+	libs := a.librariesThatImport(importedLib)
+	locs := []state.Location{}
+	for _, lib := range libs {
+		locs = append(locs, a.importsOfLibraryInLibrary(fs, importedLib, lib)...)
+	}
+	return locs
+}
+
+func (a *Analyzer) librariesThatImport(library fidlcommon.LibraryName) []fidlcommon.LibraryName {
+	libs := []fidlcommon.LibraryName{}
+	for name, lib := range a.libs {
+		for dep := range lib.deps {
+			if dep == library {
+				libs = append(libs, name)
+				break
+			}
+		}
+	}
+	return libs
+}
+
+func (a *Analyzer) importsOfLibraryInLibrary(fs *state.FileSystem, importedLib fidlcommon.LibraryName, library fidlcommon.LibraryName) []state.Location {
+	files := []state.FileID{}
+	for file := range a.libs[library].files {
+		// These files are all absolute paths, but some are to temporary input
+		// files used by the Analyzer. For these, we want to convert the path
+		// to the document URI the editor knows about.
+		if fileID, err := a.inputFileToFileID(file); err == nil {
+			files = append(files, fileID)
+		} else {
+			files = append(files, state.FileID(file))
+		}
+	}
+
+	locs := []state.Location{}
+	// Find the `using` declaration for `importedLib` in each file
+	for _, fileID := range files {
+		file, err := fs.File(fileID)
+		if err != nil {
+			// If we don't have the file in memory, read it in
+			bytes, err := ioutil.ReadFile(string(fileID))
+			if err != nil {
+				log.Printf("could not read in file `%s`: %s", fileID, err)
+				continue
+			}
+			file = string(bytes)
+		}
+
+		imports := state.ParsePlatformImportsMatch(file)
+		for _, libraryMatch := range imports {
+			if libraryMatch.Lib == importedLib {
+				locs = append(locs, state.Location{
+					FileID: fileID,
+					Range:  libraryMatch.Range,
+				})
+			}
+		}
+	}
+	return locs
+}
+
+func (a *Analyzer) typeReferencesName(t Type, name fidlcommon.Name) bool {
+	if t.FromTypeAlias != nil {
+		if *t.FromTypeAlias == name.FullyQualifiedName() {
+			return true
+		}
+		return false
+	}
+
+	switch t.Kind {
+	default:
+		return false
+
+	case ArrayType:
+		return a.typeReferencesName(t.Array.ElementType, name)
+
+	case VectorType:
+		return a.typeReferencesName(t.Vector.ElementType, name)
+
+	case StringType:
+		return false
+
+	case HandleType:
+		return false
+
+	case RequestType:
+		return t.Request.Subtype == name.FullyQualifiedName()
+
+	case PrimitiveType:
+		return false
+
+	case IdentifierType:
+		if !t.Identifier.IsDecl {
+			return t.Identifier.Identifier == name.FullyQualifiedName()
+		}
+		if t.Identifier.Kind != TypeAliasKind {
+			return false
+		}
+		return a.typeReferencesName(t.Identifier.TypeAlias.Type, name)
+	}
+}
diff --git a/fidl-lsp/analysis/references_test.go b/fidl-lsp/analysis/references_test.go
new file mode 100644
index 0000000..608477a
--- /dev/null
+++ b/fidl-lsp/analysis/references_test.go
@@ -0,0 +1,335 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis_test
+
+import (
+	"testing"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/state"
+)
+
+func TestReferencesToSymbol(t *testing.T) {
+	cases := []struct {
+		name    string
+		files   map[state.FileID]string
+		symbol  state.Symbol
+		expRefs []state.Location
+	}{
+		{
+			name: "Reference to library declaration",
+			files: map[state.FileID]string{
+				"test.fidl": `library fuchsia.test;`,
+				"other.fidl": `
+library other;
+using fuchsia.test;`,
+			},
+			symbol: state.Symbol{
+				Name: "fuchsia.test",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 0, Character: 6},
+						End:   state.Position{Line: 0, Character: 10},
+					},
+				},
+			},
+			expRefs: []state.Location{
+				{
+					FileID: state.FileID("other.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 2, Character: 6},
+						End:   state.Position{Line: 2, Character: 18},
+					},
+				},
+			},
+		},
+		{
+			name: "Reference to library import",
+			files: map[state.FileID]string{
+				"test.fidl": `library fuchsia.test;`,
+				"other.fidl": `
+library other;
+using fuchsia.test;`,
+			},
+			symbol: state.Symbol{
+				Name: "fuchsia.test",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 2, Character: 6},
+						End:   state.Position{Line: 2, Character: 18},
+					},
+				},
+			},
+			expRefs: []state.Location{
+				{
+					FileID: state.FileID("other.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 2, Character: 6},
+						End:   state.Position{Line: 2, Character: 18},
+					},
+				},
+			},
+		},
+		{
+			name: "Reference to symbol in same file",
+			files: map[state.FileID]string{
+				"test.fidl": `
+library test;
+
+struct Foo {};
+
+protocol Bar {
+	Method(Foo foo);
+//             ~~~
+};
+`,
+			},
+			symbol: state.Symbol{
+				Name: "Foo",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 3, Character: 7},
+						End:   state.Position{Line: 3, Character: 10},
+					},
+				},
+			},
+			expRefs: []state.Location{
+				{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 6, Character: 12},
+						End:   state.Position{Line: 6, Character: 15},
+					},
+				},
+			},
+		},
+		{
+			name: "Reference to symbol in other file in same library",
+			files: map[state.FileID]string{
+				"test1.fidl": `
+library test;
+
+struct Foo {};
+`,
+				"test2.fidl": `
+library test;
+
+protocol Bar {
+	Method(Foo foo);
+//             ~~~
+};
+`,
+			},
+			symbol: state.Symbol{
+				Name: "Foo",
+				Location: state.Location{
+					FileID: state.FileID("test1.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 3, Character: 7},
+						End:   state.Position{Line: 3, Character: 10},
+					},
+				},
+			},
+			expRefs: []state.Location{
+				{
+					FileID: state.FileID("test2.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 4, Character: 12},
+						End:   state.Position{Line: 4, Character: 15},
+					},
+				},
+			},
+		},
+		{
+			name: "References to symbol in other library",
+			files: map[state.FileID]string{
+				"imported.fidl": `
+library fuchsia.imported;
+
+struct Foo {};
+`,
+				"test.fidl": `
+library test;
+using fuchsia.imported;
+
+protocol Bar {
+	Method(fuchsia.imported.Foo foo);
+//                              ~~~
+};
+`,
+			},
+			symbol: state.Symbol{
+				Name: "Foo",
+				Location: state.Location{
+					FileID: state.FileID("imported.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 3, Character: 7},
+						End:   state.Position{Line: 3, Character: 10},
+					},
+				},
+			},
+			expRefs: []state.Location{
+				{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 5, Character: 29},
+						End:   state.Position{Line: 5, Character: 32},
+					},
+				},
+			},
+		},
+		{
+			name: "No references to symbol",
+			files: map[state.FileID]string{
+				"test.fidl": `
+library test;
+
+struct Foo {};
+`,
+			},
+			symbol: state.Symbol{
+				Name: "Foo",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 3, Character: 7},
+						End:   state.Position{Line: 3, Character: 10},
+					},
+				},
+			},
+			expRefs: []state.Location{},
+		},
+		{
+			name: "References to type alias",
+			files: map[state.FileID]string{
+				"zx.fidl": `
+library zx;
+
+using status = int32;
+`,
+				"test.fidl": `
+library test;
+using zx;
+
+protocol Foo {
+	Method() -> (zx.status status);
+//                         ~~~~~~
+};
+`,
+			},
+			symbol: state.Symbol{
+				Name: "status",
+				Location: state.Location{
+					FileID: state.FileID("zx.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 3, Character: 7},
+						End:   state.Position{Line: 3, Character: 10},
+					},
+				},
+			},
+			expRefs: []state.Location{
+				{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 5, Character: 24},
+						End:   state.Position{Line: 5, Character: 30},
+					},
+				},
+			},
+		},
+		{
+			name: "References in type parameters",
+			files: map[state.FileID]string{
+				"test.fidl": `
+library test;
+
+table Foo {};
+
+protocol Bar {
+	Method(vector<Foo> items);
+//                     ~~~~~
+};
+`,
+			},
+			symbol: state.Symbol{
+				Name: "Foo",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 3, Character: 6},
+						End:   state.Position{Line: 3, Character: 9},
+					},
+				},
+			},
+			expRefs: []state.Location{
+				{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 6, Character: 20},
+						End:   state.Position{Line: 6, Character: 25},
+					},
+				},
+			},
+		},
+	}
+
+	for _, ex := range cases {
+		analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+		fs := state.NewFileSystem()
+		for file, text := range ex.files {
+			fs.NewFile(file, text)
+		}
+		for file := range ex.files {
+			if err := analyzer.Analyze(fs, file); err != nil {
+				t.Fatalf("case `%s`: failed to analyze file `%s`: %s", ex.name, file, err)
+			}
+		}
+
+		refs, err := analyzer.ReferencesToSymbol(fs, ex.symbol)
+		if err != nil {
+			t.Fatalf("case `%s`: error getting references to symbol: %s", ex.name, err)
+		}
+		if len(refs) != len(ex.expRefs) {
+			t.Fatalf("case `%s`: expected %d references, got %d", ex.name, len(ex.expRefs), len(refs))
+		}
+		for _, ref := range refs {
+			found := false
+			for _, expRef := range ex.expRefs {
+				if ref == expRef {
+					found = true
+					break
+				}
+			}
+			if !found {
+				t.Errorf(
+					"case `%s`: got unexpected reference %v to symbol %s",
+					ex.name,
+					ref,
+					ex.symbol.Name,
+				)
+			}
+		}
+		for _, expRef := range ex.expRefs {
+			found := false
+			for _, ref := range refs {
+				if ref == expRef {
+					found = true
+					break
+				}
+			}
+			if !found {
+				t.Errorf(
+					"case `%s`: expected but did not find reference %v to symbol %s",
+					ex.name,
+					expRef,
+					ex.symbol.Name,
+				)
+			}
+		}
+	}
+}
diff --git a/fidl-lsp/analysis/symbol.go b/fidl-lsp/analysis/symbol.go
new file mode 100644
index 0000000..c6e1554
--- /dev/null
+++ b/fidl-lsp/analysis/symbol.go
@@ -0,0 +1,265 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"fmt"
+	"strings"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+type symbolInfo struct {
+	name       string
+	definition state.Location
+	typeInfo   Type
+}
+
+type Type struct {
+	// If IsLib is true, other fields are empty.
+	// TODO: is there a better way to represent this?
+	IsLib bool
+	// If IsMethod is true, only Attrs is set.
+	IsMethod bool
+
+	// Set if this type is actually a resolved alias
+	FromTypeAlias *string
+	Attrs         []Attribute
+
+	Kind TypeKind
+
+	Array      *ArrayTypeInfo
+	Vector     *VectorTypeInfo
+	String     *StringTypeInfo
+	Handle     *HandleTypeInfo
+	Request    *RequestTypeInfo
+	Primitive  *PrimitiveTypeInfo
+	Identifier *IdentifierTypeInfo
+}
+
+type ArrayTypeInfo struct {
+	ElementType  Type
+	ElementCount uint
+}
+
+type VectorTypeInfo struct {
+	ElementType  Type
+	ElementCount *uint
+	Nullable     bool
+}
+
+type StringTypeInfo struct {
+	ElementCount *uint
+	Nullable     bool
+}
+
+type HandleTypeInfo struct {
+	Subtype  HandleSubtype
+	Rights   uint
+	Nullable bool
+}
+
+type HandleSubtype string
+
+const (
+	Handle       HandleSubtype = "handle"
+	Bti                        = "bti"
+	Channel                    = "channel"
+	Clock                      = "clock"
+	DebugLog                   = "debuglog"
+	Event                      = "event"
+	Eventpair                  = "eventpair"
+	Exception                  = "exception"
+	Fifo                       = "fifo"
+	Guest                      = "guest"
+	Interrupt                  = "interrupt"
+	Iommu                      = "iommu"
+	Job                        = "job"
+	Pager                      = "pager"
+	PciDevice                  = "pcidevice"
+	Pmt                        = "pmt"
+	Port                       = "port"
+	Process                    = "process"
+	Profile                    = "profile"
+	Resource                   = "resource"
+	Socket                     = "socket"
+	Stream                     = "stream"
+	SuspendToken               = "suspendtoken"
+	Thread                     = "thread"
+	Time                       = "timer"
+	Vcpu                       = "vcpu"
+	Vmar                       = "vmar"
+	Vmo                        = "vmo"
+)
+
+type RequestTypeInfo struct {
+	Subtype  string
+	Nullable bool
+}
+
+type PrimitiveTypeInfo struct {
+	Subtype PrimitiveSubtype
+}
+
+type PrimitiveSubtype string
+
+const (
+	Bool    PrimitiveSubtype = "bool"
+	Int8                     = "int8"
+	Int16                    = "int16"
+	Int32                    = "int32"
+	Int64                    = "int64"
+	Uint8                    = "uint8"
+	Uint16                   = "uint16"
+	Uint32                   = "uint32"
+	Uint64                   = "uint64"
+	Float32                  = "float32"
+	Float64                  = "float64"
+)
+
+// IdentifierTypeInfo is not a perfect analog for `identifier-type` from the
+// JSON IR, because it serves as the type for both declarations of identifier
+// types and values of identifier types.
+//
+// For example, if you have declared a struct Foo, and a protocol method that
+// takes a Foo, IdentifierTypeInfo will hold the same type information for each
+// of the following symbols:
+//
+//     struct Foo {};
+//            ~~~
+//     protocol P {
+//         Method(Foo foo);
+//                ~~~ ~~~
+//     };
+//
+// So it is both a kind of "declaration type" as well as an "identifier type".
+// Iff `IsDecl` == true, it is a "declaration type"; otherwise, it is an
+// "identifier type", and `Identifier` is the name of a declaration type that
+// can be looked up in the symbol map.
+type IdentifierTypeInfo struct {
+	// If IsDecl is true, all the type information is contained in the tagged
+	// type info objects -- Kind and one of {Bits, Const, etc.}.
+	// If IsDecl is false, Identifier and Nullable are set, and Identifier is
+	// a key to the declaration of the identifier type, in the symbolMap.
+	IsDecl     bool
+	Identifier string
+	Nullable   bool
+
+	Kind IdentifierKind
+	Name string
+
+	Bits      *BitsTypeInfo
+	Const     *ConstTypeInfo
+	Enum      *EnumTypeInfo
+	TypeAlias *TypeAliasTypeInfo
+}
+
+type IdentifierKind string
+
+const (
+	BitsKind      IdentifierKind = "bits"
+	ConstKind                    = "const"
+	EnumKind                     = "enum"
+	ProtocolKind                 = "protocol"
+	ServiceKind                  = "service"
+	StructKind                   = "struct"
+	TableKind                    = "table"
+	UnionKind                    = "union"
+	TypeAliasKind                = "typeAlias"
+)
+
+type BitsTypeInfo struct {
+	Type Type
+}
+type ConstTypeInfo struct {
+	Type  Type
+	Value string
+}
+type EnumTypeInfo struct {
+	Type PrimitiveSubtype
+}
+type TypeAliasTypeInfo struct {
+	Type Type
+}
+
+func (a *Analyzer) symbolToFullyQualifiedName(fs *state.FileSystem, sym state.Symbol) (fidlcommon.Name, error) {
+	// TODO: if we want to support hovering over members (e.g. struct fields,
+	// method parameters, bits members) or protocol methods, we need to check
+	// here whether the symbol is namespaced -- whether it is inside a
+	// declaration.
+	// If it is, prepend it with that declaration's name. For example, this
+	// struct field:
+	//
+	//     struct Foo {
+	//         MyType my_field;
+	//                ~~~~~~~~
+	//     }
+	//
+	// Would become "library.name/Foo.my_field", as this is how it's stored in
+	// the symbol map.
+
+	// If `sym` is a local name (not fully-qualified), we create a FQN by
+	// attaching its library name.
+	var fqn string
+	if !strings.Contains(sym.Name, ".") {
+		file, err := fs.File(sym.Location.FileID)
+		if err != nil {
+			return fidlcommon.Name{}, fmt.Errorf("could not open file `%s`", sym.Location.FileID)
+		}
+		libName, err := state.LibraryOfFile(file)
+		if err != nil {
+			return fidlcommon.Name{}, fmt.Errorf(
+				"could not find library of symbol `%s` in file `%s`",
+				sym.Name,
+				sym.Location.FileID,
+			)
+		}
+		fqn = libName.FullyQualifiedName() + "/" + sym.Name
+	} else {
+		// If the symbol contains '.', we assume it is a fully-qualified name.
+		i := strings.LastIndex(sym.Name, ".")
+		fqn = sym.Name[:i] + "/" + sym.Name[i+1:]
+	}
+
+	// Convert `fqn` to a fidlcommon.Name.
+	name, err := fidlcommon.ReadName(fqn)
+	if err != nil {
+		return fidlcommon.Name{}, fmt.Errorf("could not read fully-qualified name `%s`: %s", fqn, err)
+	}
+	return name, nil
+}
+
+func (a *Analyzer) lookupSymbolInfo(name fidlcommon.Name) (*symbolInfo, error) {
+	lib, ok := a.libs[name.LibraryName()]
+	if !ok {
+		return nil, fmt.Errorf("unknown library `%s`", name.LibraryName().FullyQualifiedName())
+	}
+	if lib.ir == nil {
+		if err := a.importLibrary(lib.json); err != nil {
+			return nil, fmt.Errorf(
+				"error importing library `%s`: %s",
+				name.LibraryName().FullyQualifiedName(),
+				err,
+			)
+		}
+	}
+
+	// Check that we have a symbolMap for this library, and look up this
+	// symbol's definition location in that symbolMap.
+	if lib.symbols == nil {
+		return nil, fmt.Errorf(
+			"no symbol map for library `%s`",
+			name.LibraryName().FullyQualifiedName(),
+		)
+	}
+	symInfo, ok := lib.symbols[name.FullyQualifiedName()]
+	if !ok {
+		return nil, fmt.Errorf("could not find symbol `%s`", name.FullyQualifiedName())
+	}
+
+	return symInfo, nil
+}
diff --git a/fidl-lsp/analysis/type.go b/fidl-lsp/analysis/type.go
new file mode 100644
index 0000000..fcec389
--- /dev/null
+++ b/fidl-lsp/analysis/type.go
@@ -0,0 +1,105 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package analysis
+
+import (
+	"fmt"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+// TypeOfSymbol returns the type information of `sym`.
+//
+// analysis.Type contains all the information needed to create a human-readable
+// description of the type.
+func (a *Analyzer) TypeOfSymbol(fs *state.FileSystem, sym state.Symbol) (Type, error) {
+	// If `sym` is a library name, return a `library`-kinded TypeInfo.
+	libName, err := fidlcommon.ReadLibraryName(sym.Name)
+	if err == nil {
+		if _, isLib := a.libs[libName]; isLib {
+			return Type{
+				IsLib: true,
+			}, nil
+		}
+	}
+
+	// Otherwise, we assume it is a local or fully-qualified name
+	name, err := a.symbolToFullyQualifiedName(fs, sym)
+	if err != nil {
+		return Type{}, fmt.Errorf(
+			"could not convert symbol `%s` to fully-qualified name: %s",
+			sym.Name,
+			err,
+		)
+	}
+
+	symInfo, err := a.lookupSymbolInfo(name)
+	if err != nil {
+		return Type{}, fmt.Errorf("could not find type of symbol `%s`: %s", name.FullyQualifiedName(), err)
+	}
+
+	// Resolve identifier type, if necessary.
+	if symInfo.typeInfo.Kind == IdentifierType && !symInfo.typeInfo.Identifier.IsDecl {
+		// This means that rather than being a declaration, symInfo is a value
+		// of an identifier type, so we lookup that type's info based on the
+		// type name.
+		typeName, err := fidlcommon.ReadName(symInfo.typeInfo.Identifier.Identifier)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"invalid identifier type `%s`: %s",
+				symInfo.typeInfo.Identifier.Identifier,
+				err,
+			)
+		}
+		t, err := a.lookupSymbolInfo(typeName)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"could not find identifier type `%s` of symbol `%s`: %s",
+				typeName.FullyQualifiedName(),
+				name.FullyQualifiedName(),
+				err,
+			)
+		}
+		identifierType := t.typeInfo
+		identifierType.Identifier.IsDecl = false
+		identifierType.Identifier.Nullable = symInfo.typeInfo.Identifier.Nullable
+		identifierType.Identifier.Identifier = symInfo.typeInfo.Identifier.Identifier
+		return identifierType, nil
+	}
+
+	// Resolve aliased identifier type, if necessary.
+	if symInfo.typeInfo.Kind == IdentifierType &&
+		symInfo.typeInfo.Identifier.Kind == TypeAliasKind &&
+		symInfo.typeInfo.Identifier.TypeAlias.Type.Kind == IdentifierType {
+		// This means that `sym` is a type alias to an identifier type, so we
+		// need to lookup that identifier type's info based on the type name.
+		aliasedType := symInfo.typeInfo.Identifier.TypeAlias.Type
+		typeName, err := fidlcommon.ReadName(aliasedType.Identifier.Identifier)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"invalid identifier type `%s`: %s",
+				aliasedType.Identifier.Identifier,
+				err,
+			)
+		}
+		t, err := a.lookupSymbolInfo(typeName)
+		if err != nil {
+			return Type{}, fmt.Errorf(
+				"could not find identifier type `%s` of symbol `%s`: %s",
+				typeName.FullyQualifiedName(),
+				name.FullyQualifiedName(),
+				err,
+			)
+		}
+		identifierType := t.typeInfo
+		identifierType.Identifier.Nullable = aliasedType.Identifier.Nullable
+		identifierType.Identifier.Identifier = aliasedType.Identifier.Identifier
+		symInfo.typeInfo.Identifier.TypeAlias.Type = identifierType
+	}
+
+	return symInfo.typeInfo, nil
+}
diff --git a/fidl-lsp/go.mod b/fidl-lsp/go.mod
new file mode 100644
index 0000000..9ab5fda
--- /dev/null
+++ b/fidl-lsp/go.mod
@@ -0,0 +1,8 @@
+module fidl-lsp
+
+go 1.14
+
+require (
+	github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d
+	github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37
+)
diff --git a/fidl-lsp/langserver/definition.go b/fidl-lsp/langserver/definition.go
new file mode 100644
index 0000000..d0468ea
--- /dev/null
+++ b/fidl-lsp/langserver/definition.go
@@ -0,0 +1,45 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/state"
+)
+
+func (h *LangHandler) handleDefinition(params lsp.TextDocumentPositionParams) ([]lsp.Location, error) {
+	sym, err := h.fs.SymbolAtPos(
+		state.FileID(params.TextDocument.URI),
+		state.Position{Line: params.Position.Line, Character: params.Position.Character},
+	)
+	if err != nil {
+		h.log.Printf(
+			"could not find symbol at position `%#v` in document `%s`\n",
+			params.Position,
+			params.TextDocument.URI,
+		)
+		return nil, err
+	}
+
+	locs, err := h.analyzer.DefinitionOfSymbol(h.fs, sym)
+	if err != nil {
+		h.log.Printf("error on definition: %s\n", err)
+		return nil, err
+	}
+
+	// Convert state.Locations --> lsp.Locations
+	lspLocs := make([]lsp.Location, len(locs))
+	for i, loc := range locs {
+		lspLocs[i] = lsp.Location{
+			URI: lsp.DocumentURI(loc.FileID),
+			Range: lsp.Range{
+				Start: lsp.Position{Line: loc.Range.Start.Line, Character: loc.Range.Start.Character},
+				End:   lsp.Position{Line: loc.Range.End.Line, Character: loc.Range.End.Character},
+			},
+		}
+	}
+	return lspLocs, nil
+}
diff --git a/fidl-lsp/langserver/diagnostics.go b/fidl-lsp/langserver/diagnostics.go
new file mode 100644
index 0000000..01d1212
--- /dev/null
+++ b/fidl-lsp/langserver/diagnostics.go
@@ -0,0 +1,87 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	fidlcommon "fidl-lsp/third_party/common"
+	"github.com/sourcegraph/go-lsp"
+	"github.com/sourcegraph/jsonrpc2"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/state"
+)
+
+func (h *LangHandler) publishDiagnostics(ctx context.Context, conn *jsonrpc2.Conn, uri lsp.DocumentURI) {
+	diags, err := h.getDiagnosticsForLibraryWithFile(uri)
+	if err != nil {
+		h.log.Printf("error publishing diagnostics: %s\n", err)
+		return
+	}
+
+	for fileURI, fileDiags := range diags {
+		h.Notify(ctx, conn, "textDocument/publishDiagnostics", lsp.PublishDiagnosticsParams{
+			URI:         fileURI,
+			Diagnostics: fileDiags,
+		})
+	}
+}
+
+func (h *LangHandler) getDiagnosticsForLibraryWithFile(uri lsp.DocumentURI) (map[lsp.DocumentURI][]lsp.Diagnostic, error) {
+	file, err := h.fs.File(state.FileID(uri))
+	if err != nil {
+		return nil, fmt.Errorf("could not find file `%s`: %s", uri, err)
+	}
+	libraryName, err := state.LibraryOfFile(file)
+	if err != nil {
+		return nil, fmt.Errorf("could not find library name of file `%s`: %s", uri, err)
+	}
+
+	diags, err := h.analyzer.DiagnosticsOnLibrary(fidlcommon.LibraryName(libraryName))
+	if err != nil {
+		return nil, fmt.Errorf("error getting diagnostics: %s", err)
+	}
+
+	// Convert analysis.Diagnostics --> lsp.Diagnostics
+	lspDiags := make(map[lsp.DocumentURI][]lsp.Diagnostic)
+	for fileID, fileDiags := range diags {
+		fileURI := lsp.DocumentURI(fileID)
+		lspDiags[fileURI] = []lsp.Diagnostic{}
+
+		for _, diag := range fileDiags {
+			var severity lsp.DiagnosticSeverity
+			message := diag.Message
+
+			switch diag.Source {
+			case analysis.Fidlc:
+				diagType := strings.Split(diag.Category, "/")[1]
+				if diagType == "error" {
+					severity = lsp.Error
+				} else {
+					severity = lsp.Warning
+				}
+			case analysis.FidlLint:
+				severity = lsp.Warning
+				if len(diag.Suggestions) > 0 {
+					message = message + "; " + diag.Suggestions[0].Description
+				}
+			}
+
+			lspDiags[fileURI] = append(lspDiags[fileURI], lsp.Diagnostic{
+				Range: lsp.Range{
+					Start: lsp.Position{Line: diag.StartLine - 1, Character: diag.StartChar},
+					End:   lsp.Position{Line: diag.EndLine - 1, Character: diag.EndChar},
+				},
+				Severity: severity,
+				Source:   string(diag.Source),
+				Message:  message,
+			})
+		}
+	}
+	return lspDiags, nil
+}
diff --git a/fidl-lsp/langserver/diagnostics_test.go b/fidl-lsp/langserver/diagnostics_test.go
new file mode 100644
index 0000000..efacd87
--- /dev/null
+++ b/fidl-lsp/langserver/diagnostics_test.go
@@ -0,0 +1,171 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"testing"
+
+	"github.com/sourcegraph/go-lsp"
+)
+
+func TestFileWithNoDiagnostics(t *testing.T) {
+	handler := NewHandlerWithFiles(TestFile{
+		Name: "test.fidl",
+		Text: `// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library test;
+const uint8 VALID_NAME = 1;
+
+struct Foo {
+	uint8 field_name;
+};
+
+protocol Bar {
+	Baz(string:256 message);
+};
+`,
+	})
+
+	diags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+	if err != nil {
+		t.Fatalf("failed to publish diagnostics: %s", err)
+	}
+	if len(diags["test.fidl"]) > 0 {
+		t.Fatalf("incorrect number of diagnostics; expected 0, actual %d", len(diags))
+	}
+}
+
+func TestDiagnosticsFidlcErrors(t *testing.T) {
+	handler := NewHandlerWithFiles(TestFile{
+		Name: "test.fidl",
+		Text: `// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library test;
+const uint8 DUPLICATE_NAME = 0;
+const uint8 DUPLICATE_NAME = 1;
+//          ~~~~~~~~~~~~~~ expected range of diagnostic
+`,
+	})
+
+	allDiags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+	if err != nil {
+		t.Fatalf("failed to publish diagnostics: %s", err)
+	}
+	diags := allDiags["test.fidl"]
+	if len(diags) != 1 {
+		t.Fatalf("incorrect number of diagnostics %d; expected 1", len(diags))
+	}
+	if diags[0].Source != "fidlc" {
+		t.Errorf("unexpected source of diagnostic %s; expected `fidlc`", diags[0].Source)
+	}
+	expRange := lsp.Range{
+		Start: lsp.Position{Line: 6, Character: 12},
+		End:   lsp.Position{Line: 6, Character: 26},
+	}
+	if diags[0].Range != expRange {
+		t.Errorf("unexpected range of diagnostic %v; expected %v", diags[0].Range, expRange)
+	}
+	if diags[0].Severity != lsp.Error {
+		t.Errorf("unexpected severity of diagnostic %v; expected %v", diags[0].Severity, lsp.Error)
+	}
+}
+
+func TestDiagnosticsFidlcWarnings(t *testing.T) {
+	handler := NewHandlerWithFiles(TestFile{
+		Name: "test.fidl",
+		Text: `// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library test;
+
+[layout = "Simple"]
+//~~~~~~~~~~~~~~~~ expected range of diagnostic
+struct S {
+	uint8 foo;
+};
+`,
+	})
+
+	allDiags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+	if err != nil {
+		t.Fatalf("failed to publish diagnostics: %s", err)
+	}
+	diags := allDiags["test.fidl"]
+	if len(diags) != 1 {
+		t.Fatalf("incorrect number of diagnostics %d; expected 1", len(diags))
+	}
+	if diags[0].Source != "fidlc" {
+		t.Errorf("unexpected source of diagnostic %s; expected `fidlc`", diags[0].Source)
+	}
+	expRange := lsp.Range{
+		Start: lsp.Position{Line: 6, Character: 1},
+		End:   lsp.Position{Line: 6, Character: 18},
+	}
+	if diags[0].Range != expRange {
+		t.Errorf("unexpected range of diagnostic %v; expected %v", diags[0].Range, expRange)
+	}
+	if diags[0].Severity != lsp.Warning {
+		t.Errorf("unexpected severity of diagnostic %v; expected %v", diags[0].Severity, lsp.Warning)
+	}
+}
+
+func TestDiagnosticsFidlLint(t *testing.T) {
+	handler := NewHandlerWithFiles(TestFile{
+		Name: "test.fidl",
+		Text: `
+library x;
+
+enum my_enum {
+    IncorrectCamelCase = 1;
+};
+`,
+	})
+
+	allDiags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+	if err != nil {
+		t.Fatalf("failed to publish diagnostics: %s", err)
+	}
+	diags := allDiags["test.fidl"]
+	if len(diags) != 4 {
+		t.Fatalf("incorrect number of diagnostics %d; expected 4", len(diags))
+	}
+	for _, diag := range diags {
+		if diag.Source != "fidl-lint" {
+			t.Errorf("unexpected source of diagnostic %s; expected `fidl-lint`", diag.Source)
+		}
+		if diag.Severity != lsp.Warning {
+			t.Errorf("unexpected severity of diagnostic %v; expected %v", diag.Severity, lsp.Warning)
+		}
+	}
+}
+
+func TestErrorAndLint(t *testing.T) {
+	handler := NewHandlerWithFiles(TestFile{
+		Name: "test.fidl",
+		Text: `
+// expected lint: need Fuchsia copyright notice
+library test;
+
+protocol P {
+	DuplicateMethodName();
+	DuplicateMethodName();
+// expected error: duplicate method name
+};
+`,
+	})
+
+	diags, err := handler.getDiagnosticsForLibraryWithFile(lsp.DocumentURI("test.fidl"))
+	if err != nil {
+		t.Fatalf("failed to publish diagnostics: %s", err)
+	}
+	if len(diags["test.fidl"]) != 2 {
+		t.Fatalf("incorrect number of diagnostics %d; expected 3", len(diags["test.fidl"]))
+	}
+}
diff --git a/fidl-lsp/langserver/files.go b/fidl-lsp/langserver/files.go
new file mode 100644
index 0000000..c480a0c
--- /dev/null
+++ b/fidl-lsp/langserver/files.go
@@ -0,0 +1,43 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/state"
+)
+
+func (h *LangHandler) handleOpenFile(params lsp.DidOpenTextDocumentParams) {
+	h.fs.NewFile(state.FileID(params.TextDocument.URI), params.TextDocument.Text)
+}
+
+func (h *LangHandler) handleCloseFile(params lsp.DidCloseTextDocumentParams) {
+	h.fs.DeleteFile(state.FileID(params.TextDocument.URI))
+}
+
+func (h *LangHandler) handleDidChange(params lsp.DidChangeTextDocumentParams) {
+	// Convert from []lsp.TextDocumentContentChangeEvent --> []state.Change
+	changes := make([]state.Change, len(params.ContentChanges))
+	for i, lspChange := range params.ContentChanges {
+		changes[i] = state.Change{
+			Range: state.Range{
+				Start: state.Position{
+					Line:      lspChange.Range.Start.Line,
+					Character: lspChange.Range.Start.Character,
+				},
+				End: state.Position{
+					Line:      lspChange.Range.End.Line,
+					Character: lspChange.Range.End.Character,
+				},
+			},
+			NewContent: lspChange.Text,
+		}
+	}
+
+	if err := h.fs.ApplyChanges(state.FileID(params.TextDocument.URI), changes); err != nil {
+		h.log.Printf("error on ApplyChanges: %s", err)
+	}
+}
diff --git a/fidl-lsp/langserver/format.go b/fidl-lsp/langserver/format.go
new file mode 100644
index 0000000..a7b86c3
--- /dev/null
+++ b/fidl-lsp/langserver/format.go
@@ -0,0 +1,38 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"strings"
+
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/state"
+)
+
+// handleFormat asks the Analyzer for the formatted version of the document
+// specified in `params` and returns any resulting TextEdits, or an error.
+//
+// In practice, since fidl-format only runs on an entire document, when
+// successful this will always return a single TextEdit spanning (and replacing)
+// the entire document with its formatted version.
+func (h *LangHandler) handleFormat(params lsp.DocumentFormattingParams) ([]lsp.TextEdit, error) {
+	formatted, err := h.analyzer.FormatFile(h.fs, state.FileID(params.TextDocument.URI))
+	if err != nil {
+		return nil, err
+	}
+
+	lines := strings.Split(formatted, "\n")
+	lastLine := lines[len(lines)-1]
+	lastChar := len([]rune(lastLine))
+
+	return []lsp.TextEdit{{
+		Range: lsp.Range{
+			Start: lsp.Position{},
+			End:   lsp.Position{Line: len(lines), Character: lastChar},
+		},
+		NewText: formatted,
+	}}, nil
+}
diff --git a/fidl-lsp/langserver/format_test.go b/fidl-lsp/langserver/format_test.go
new file mode 100644
index 0000000..6dbcb04
--- /dev/null
+++ b/fidl-lsp/langserver/format_test.go
@@ -0,0 +1,99 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"testing"
+
+	"github.com/sourcegraph/go-lsp"
+)
+
+func TestFormat(t *testing.T) {
+	handler := NewHandlerWithFiles(TestFile{
+		Name: "test.fidl",
+		Text: `
+library example;
+struct S { int32 field; };
+protocol P {
+UnindentedMethod  ( uint8 param1 , string param2 )  ;
+};
+table T   { 1  :uint u;
+};
+`,
+	})
+
+	edits, err := handler.handleFormat(lsp.DocumentFormattingParams{
+		TextDocument: lsp.TextDocumentIdentifier{
+			URI: lsp.DocumentURI("test.fidl"),
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to format file: %s", err)
+	}
+
+	if len(edits) != 1 {
+		t.Errorf("incorrect number of formatting edits: expected 1, got %d", len(edits))
+	}
+	expRange := lsp.Range{
+		Start: lsp.Position{Line: 0, Character: 0},
+		End:   lsp.Position{Line: 14, Character: 0},
+	}
+	if edits[0].Range != expRange {
+		t.Errorf(
+			"formatted range does not span entire file: expected %v, got %v",
+			expRange,
+			edits[0].Range,
+		)
+	}
+
+	expText := `library example;
+
+struct S {
+    int32 field;
+};
+
+protocol P {
+    UnindentedMethod(uint8 param1, string param2);
+};
+
+table T {
+    1: uint u;
+};
+`
+	if edits[0].NewText != expText {
+		t.Errorf("formatted text is incorrect: expected %s, got %s", expText, edits[0].NewText)
+	}
+}
+
+func TestFormatFileWithErrors(t *testing.T) {
+	handler := NewHandlerWithFiles(TestFile{
+		Name: "test.fidl",
+		Text: `library example`,
+	})
+
+	_, err := handler.handleFormat(lsp.DocumentFormattingParams{
+		TextDocument: lsp.TextDocumentIdentifier{
+			URI: lsp.DocumentURI("test.fidl"),
+		},
+	})
+
+	if err == nil {
+		t.Errorf("formatting file with errors did not fail")
+	}
+}
+
+func TestFormatNonexistentFile(t *testing.T) {
+	handler := NewHandlerWithFiles()
+
+	_, err := handler.handleFormat(lsp.DocumentFormattingParams{
+		TextDocument: lsp.TextDocumentIdentifier{
+			URI: lsp.DocumentURI("test.fidl"),
+		},
+	})
+
+	if err == nil {
+		t.Errorf("formatting nonexistent file did not fail")
+	}
+}
diff --git a/fidl-lsp/langserver/handler.go b/fidl-lsp/langserver/handler.go
new file mode 100644
index 0000000..686053c
--- /dev/null
+++ b/fidl-lsp/langserver/handler.go
@@ -0,0 +1,293 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log"
+	"os"
+
+	fidlcommon "fidl-lsp/third_party/common"
+	"github.com/sourcegraph/go-lsp"
+	"github.com/sourcegraph/jsonrpc2"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/state"
+)
+
+// LspServer is a wrapper around a LangHandler.
+// It is the entry point for all LSP communication with the client.
+type LspServer struct {
+	jsonrpc2.Handler
+}
+
+// LangHandler implements jsonrpc2.Handler, an interface with one Handle method.
+// It fields all LSP requests from the client, and sends back responses and
+// notifications.
+//
+// LangHandler has a *state.FileSystem, which keeps track of the the language
+// editor's open files (in the future, to support concurrency, this state can be
+// wrapped in e.g. an RWLock).
+//
+// LangHandler also has an *analysis.Analyzer,
+// which is used to compile and extract insights from files being edited.
+type LangHandler struct {
+	cfg      Config
+	init     *lsp.InitializeParams
+	shutdown bool
+
+	log *log.Logger
+
+	fs *state.FileSystem
+
+	analyzer *analysis.Analyzer
+}
+
+// Assert that LangHandler implements the jsonrpc2.Handler interface
+var _ jsonrpc2.Handler = (*LangHandler)(nil)
+
+type Config struct {
+	serverCapabilities serverCapabilities
+
+	// The base URI for reference links. Currently we linkify Fuchsia platform
+	// library imports, and the links point to the SDK docs.
+	baseRefURI string
+}
+
+func (c *Config) FormatRefURI(libraryName fidlcommon.LibraryName) string {
+	return c.baseRefURI + libraryName.FullyQualifiedName()
+}
+
+// NewDefaultConfig returns a default configuration for the language server.
+func NewDefaultConfig() Config {
+	return Config{
+		serverCapabilities: serverCapabilities{
+			DefinitionProvider:         true,
+			DocumentFormattingProvider: true,
+			DocumentLinkProvider:       &documentLinkOptions{},
+			HoverProvider:              true,
+			ReferencesProvider:         true,
+			TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{
+				Options: &lsp.TextDocumentSyncOptions{
+					OpenClose: true,
+					Change:    lsp.TDSKIncremental,
+				},
+			},
+		},
+		baseRefURI: "https://fuchsia.dev/reference/fidl/",
+	}
+}
+
+// NewLangHandler returns a LangHandler with the given configuration, logger,
+// and set of CompiledLibraries.
+func NewLangHandler(cfg Config, trace *log.Logger, analyzer *analysis.Analyzer) *LangHandler {
+	return &LangHandler{
+		log:      trace,
+		cfg:      cfg,
+		fs:       state.NewFileSystem(),
+		analyzer: analyzer,
+	}
+}
+
+// Handle fields an LSP request or notification.
+// If there is an internal server error, it will be logged and an empty response
+// will be sent to the client. If there is an error in the request, an error
+// reponse will be sent.
+func (h *LangHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {
+	if resp, handleErr := h.handle(ctx, conn, req); handleErr == nil {
+		if resp != nil {
+			if err := conn.Reply(ctx, req.ID, resp); err != nil {
+				h.log.Printf("error on conn.Reply: %s\n", err)
+			}
+		}
+	} else {
+		switch handleErr.(type) {
+		case *jsonrpc2.Error:
+			h.log.Printf("got a request we couldn't handle: %s\n", req.Method)
+			if err := conn.ReplyWithError(ctx, req.ID, handleErr.(*jsonrpc2.Error)); err != nil {
+				h.log.Printf("error on conn.ReplyWithError: %s\n", err)
+			}
+		default:
+			// If the error is not a jsonrpc2.Error, this means that we had an
+			// internal server error and were unable to meet the request. Rather
+			// than pass that error on to the client, we should log the error
+			// and reply with an empty response so the client is not waiting for
+			// a response.
+			h.log.Printf("err on handling `%s`: %s\n", req.Method, handleErr)
+			if err := conn.Reply(ctx, req.ID, resp); err != nil {
+				h.log.Printf("error on conn.Reply: %s\n", err)
+			}
+		}
+	}
+}
+
+// Notify sends an LSP notification.
+func (h *LangHandler) Notify(ctx context.Context, conn *jsonrpc2.Conn, method string, notif interface{}) {
+	if err := conn.Notify(ctx, method, notif); err != nil {
+		h.log.Printf("error on conn.Reply: %s\n", err)
+	}
+}
+
+// The general pattern of the `handle` callback is as follows:
+//
+// * receive a notification from the client indicating a change of state -->
+//   pass the change to the FileSystem -->
+//   trigger a re-analysis of the file that was changed -->
+//   publish diagnostics on that file.
+//
+// * receive a request from the client -->
+//   request the relevant analysis from the Analyzer, passing it a snapshot of
+//   the state of the FileSystem -->
+//   wrap the analysis in relevant LSP type/object and send to the client.
+func (h *LangHandler) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) {
+	// TODO: locking over `h`
+	switch req.Method {
+	case "initialize":
+		// TODO: if server receives any request before `initialize`, it should
+		// respond with error code: -32002
+		if h.init != nil {
+			return nil, &jsonrpc2.Error{
+				Code:    jsonrpc2.CodeInvalidRequest,
+				Message: "language server is already initialized",
+			}
+		}
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params lsp.InitializeParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+		h.init = &params
+		return initializeResult{Capabilities: h.cfg.serverCapabilities}, nil
+
+	case "initialized":
+		// A notification that the client is ready to receive requests. Ignore
+		return nil, nil
+
+	case "shutdown":
+		// TODO: if server receives any request after `shutdown`, it should
+		// respond with an error: InvalidRequest.
+		h.shutdown = true
+		h.analyzer.Cleanup()
+		return nil, nil
+
+	case "exit":
+		if h.shutdown {
+			os.Exit(0)
+		} else {
+			os.Exit(1)
+		}
+		return nil, nil
+
+	case "textDocument/didOpen":
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params lsp.DidOpenTextDocumentParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+
+		h.handleOpenFile(params)
+		if err := h.analyzer.Analyze(h.fs, state.FileID(params.TextDocument.URI)); err != nil {
+			h.log.Println(err)
+		}
+		h.publishDiagnostics(ctx, conn, params.TextDocument.URI)
+		return nil, nil
+
+	case "textDocument/didClose":
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params lsp.DidCloseTextDocumentParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+
+		h.handleCloseFile(params)
+		// TODO: remove diagnostics associated with this file?
+		return nil, nil
+
+	case "textDocument/didChange":
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params lsp.DidChangeTextDocumentParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+
+		h.handleDidChange(params)
+		if err := h.analyzer.Analyze(h.fs, state.FileID(params.TextDocument.URI)); err != nil {
+			h.log.Println(err)
+		}
+		h.publishDiagnostics(ctx, conn, params.TextDocument.URI)
+		return nil, nil
+
+	case "textDocument/definition":
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params lsp.TextDocumentPositionParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+
+		return h.handleDefinition(params)
+
+	case "textDocument/documentLink":
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params documentLinkParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+
+		return h.handleDocumentLinks(params)
+
+	case "textDocument/formatting":
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params lsp.DocumentFormattingParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+
+		return h.handleFormat(params)
+
+	case "textDocument/hover":
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params lsp.TextDocumentPositionParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+
+		return h.handleHover(params)
+
+	case "textDocument/references":
+		if req.Params == nil {
+			return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams}
+		}
+		var params lsp.TextDocumentPositionParams
+		if err := json.Unmarshal(*req.Params, &params); err != nil {
+			return nil, err
+		}
+
+		return h.handleReferences(params)
+
+	default:
+		return nil, &jsonrpc2.Error{
+			Code:    jsonrpc2.CodeMethodNotFound,
+			Message: fmt.Sprintf("method not supported: %s", req.Method),
+		}
+	}
+}
diff --git a/fidl-lsp/langserver/hover.go b/fidl-lsp/langserver/hover.go
new file mode 100644
index 0000000..5977b4c
--- /dev/null
+++ b/fidl-lsp/langserver/hover.go
@@ -0,0 +1,318 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"fmt"
+
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/state"
+)
+
+// handleHover asks the FileSystem for the symbol at the location specified in
+// `params`, and then asks the Analyzer for the type of that symbol.
+//
+// It then converts that type to a human-readable string representation that
+// will be displayed as a tooltip, including doc comments, other attributes on
+// the type, and the type itself. For example, given this FIDL:
+//
+//     library example;
+//
+//     /// Foo is a struct.
+//     [Attribute = "Value"]
+//     struct Foo {};
+//
+//     protocol Bar {
+//         Method(Foo foo);
+//                ~~~
+//     }
+//
+// Hovering over the `~~~` would give you this tooltip:
+//
+//     Foo is a struct.       // In plain text
+//     [Attribute = "Value"]  // In monospace font
+//     struct example/Foo
+//
+func (h *LangHandler) handleHover(params lsp.TextDocumentPositionParams) (lsp.Hover, error) {
+	sym, err := h.fs.SymbolAtPos(
+		state.FileID(params.TextDocument.URI),
+		state.Position{Line: params.Position.Line, Character: params.Position.Character},
+	)
+	if err != nil {
+		h.log.Printf(
+			"could not find symbol at position `%#v` in document `%s`\n",
+			params.Position,
+			params.TextDocument.URI,
+		)
+		return lsp.Hover{}, err
+	}
+
+	symType, err := h.analyzer.TypeOfSymbol(h.fs, sym)
+	if err != nil {
+		h.log.Printf("could not get type of symbol `%s`: %s", sym.Name, err)
+		return lsp.Hover{}, err
+	}
+
+	// Convert SymbolType --> []lsp.MarkedString
+	typeDescription, err := symbolTypeToMarkedStrings(sym.Name, symType)
+	if err != nil {
+		h.log.Printf("could not get string representation of symbol `%s`'s type: %s", sym.Name, err)
+		return lsp.Hover{}, err
+	}
+
+	return lsp.Hover{
+		Contents: typeDescription,
+		Range: &lsp.Range{
+			Start: lsp.Position{
+				Line:      sym.Location.Range.Start.Line,
+				Character: sym.Location.Range.Start.Character,
+			},
+			End: lsp.Position{
+				Line:      sym.Location.Range.End.Line,
+				Character: sym.Location.Range.End.Character,
+			},
+		},
+	}, nil
+}
+
+func newFIDLString(s string) lsp.MarkedString {
+	return lsp.MarkedString{
+		Language: "fidl",
+		Value:    s,
+	}
+}
+
+func symbolTypeToMarkedStrings(name string, symType analysis.Type) ([]lsp.MarkedString, error) {
+	if symType.IsLib {
+		return []lsp.MarkedString{{
+			Language: "fidl",
+			Value:    fmt.Sprintf("library %s", name),
+		}}, nil
+	}
+
+	res := []lsp.MarkedString{}
+
+	var docStr *lsp.MarkedString
+	attrList := "["
+	firstAttr := true
+	// Add attributes as a marked string
+	for _, attr := range symType.Attrs {
+		if attr.Name == "Doc" {
+			rawMarkedStr := lsp.RawMarkedString(attr.Value)
+			docStr = &rawMarkedStr
+		} else {
+			if !firstAttr {
+				attrList += ", "
+			}
+			firstAttr = false
+			if attr.Value != "" {
+				attrList += fmt.Sprintf("%s = \"%s\"", attr.Name, attr.Value)
+			} else {
+				attrList += attr.Name
+			}
+		}
+	}
+	attrList += "]"
+	// Prepend doc string if there is one
+	if docStr != nil {
+		res = append([]lsp.MarkedString{*docStr}, res...)
+	}
+	if !firstAttr {
+		res = append(res, lsp.MarkedString{
+			Language: "fidl",
+			Value:    attrList,
+		})
+	}
+
+	switch symType.Kind {
+	default:
+		return nil, fmt.Errorf("unknown kind of symbol: %s", symType.Kind)
+
+	case analysis.ArrayType:
+		if symType.Array == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.VectorType:
+		if symType.Vector == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.StringType:
+		if symType.String == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.HandleType:
+		if symType.Handle == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.RequestType:
+		if symType.Request == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.PrimitiveType:
+		if symType.Primitive == nil {
+			goto TypeInfoNotSet
+		}
+		res = append(res, newFIDLString(shortName(symType)))
+
+	case analysis.IdentifierType:
+		if symType.Identifier == nil {
+			goto TypeInfoNotSet
+		}
+
+		if !symType.Identifier.IsDecl {
+			res = append(res, newFIDLString(shortName(symType)))
+			break
+		}
+
+		switch symType.Identifier.Kind {
+		default:
+			return nil, fmt.Errorf("unknown identifier kind: %s", symType.Identifier.Kind)
+
+		case analysis.BitsKind:
+			if symType.Identifier.Bits == nil {
+				goto IdentifierTypeInfoNotSet
+			}
+			res = append(res, newFIDLString(
+				fmt.Sprintf(
+					"bits %s: %s",
+					symType.Identifier.Name,
+					shortName(symType.Identifier.Bits.Type),
+				),
+			))
+
+		case analysis.EnumKind:
+			if symType.Identifier.Enum == nil {
+				goto IdentifierTypeInfoNotSet
+			}
+			res = append(res, newFIDLString(
+				fmt.Sprintf(
+					"enum %s: %s",
+					symType.Identifier.Name,
+					symType.Identifier.Enum.Type,
+				),
+			))
+
+		case analysis.ConstKind:
+			if symType.Identifier.Const == nil {
+				goto IdentifierTypeInfoNotSet
+			}
+			res = append(res, newFIDLString(
+				fmt.Sprintf(
+					"const %s %s = `%s`",
+					shortName(symType.Identifier.Const.Type),
+					symType.Identifier.Name,
+					symType.Identifier.Const.Value,
+				),
+			))
+
+		case analysis.TypeAliasKind:
+			if symType.Identifier.TypeAlias == nil {
+				goto IdentifierTypeInfoNotSet
+			}
+			res = append(res, newFIDLString(
+				fmt.Sprintf(
+					"%s: alias to %s",
+					symType.Identifier.Name,
+					shortName(symType.Identifier.TypeAlias.Type),
+				),
+			))
+
+		case analysis.ProtocolKind,
+			analysis.ServiceKind,
+			analysis.StructKind,
+			analysis.TableKind,
+			analysis.UnionKind:
+			res = append(res, newFIDLString(
+				fmt.Sprintf("%s %s", symType.Identifier.Kind, symType.Identifier.Name),
+			))
+		}
+	}
+
+	return res, nil
+
+TypeInfoNotSet:
+	return nil, fmt.Errorf(
+		"symbol is tagged as a `%s` but `%s` type info not set",
+		symType.Kind,
+		symType.Kind,
+	)
+IdentifierTypeInfoNotSet:
+	return nil, fmt.Errorf(
+		"symbol is tagged as a `%s` but `%s` type info not set",
+		symType.Identifier.Kind,
+		symType.Identifier.Kind,
+	)
+}
+
+func shortName(t analysis.Type) string {
+	switch t.Kind {
+	default:
+		return "unknown type"
+
+	case analysis.ArrayType:
+		return fmt.Sprintf("array<%s>:%d", shortName(t.Array.ElementType), t.Array.ElementCount)
+
+	case analysis.VectorType:
+		str := fmt.Sprintf("vector<%s>", shortName(t.Vector.ElementType))
+		if t.Vector.ElementCount != nil {
+			str += fmt.Sprintf(":%d", *t.Vector.ElementCount)
+		}
+		if t.Vector.Nullable {
+			str += "?"
+		}
+		return str
+
+	case analysis.StringType:
+		str := "string"
+		if t.String.ElementCount != nil {
+			str += fmt.Sprintf(":%d", *t.String.ElementCount)
+		}
+		if t.String.Nullable {
+			str += "?"
+		}
+		return str
+
+	case analysis.RequestType:
+		str := fmt.Sprintf("request<%s>", t.Request.Subtype)
+		if t.Request.Nullable {
+			str += "?"
+		}
+		return str
+
+	case analysis.HandleType:
+		str := fmt.Sprintf("handle<%s>", t.Handle.Subtype)
+		if t.Handle.Nullable {
+			str += "?"
+		}
+		return str
+
+	case analysis.PrimitiveType:
+		return string(t.Primitive.Subtype)
+
+	case analysis.IdentifierType:
+		var str string
+		if t.Identifier.IsDecl {
+			str = t.Identifier.Name
+		} else {
+			str = t.Identifier.Identifier
+		}
+		if t.Identifier.Nullable {
+			str += "?"
+		}
+		return str
+	}
+}
diff --git a/fidl-lsp/langserver/hover_test.go b/fidl-lsp/langserver/hover_test.go
new file mode 100644
index 0000000..7b0bf0a
--- /dev/null
+++ b/fidl-lsp/langserver/hover_test.go
@@ -0,0 +1,366 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"testing"
+
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/analysis"
+)
+
+func TestHover(t *testing.T) {
+	var elementCount uint = 10
+
+	cases := []struct {
+		symName   string
+		symType   analysis.Type
+		hoverText []lsp.MarkedString
+	}{
+		{
+			symName: "fuchsia.test",
+			symType: analysis.Type{
+				IsLib: true,
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("library fuchsia.test")},
+		},
+		{
+			symName: "array_value",
+			symType: analysis.Type{
+				Kind: analysis.ArrayType,
+				Array: &analysis.ArrayTypeInfo{
+					ElementType: analysis.Type{
+						Kind: analysis.PrimitiveType,
+						Primitive: &analysis.PrimitiveTypeInfo{
+							Subtype: analysis.Uint32,
+						},
+					},
+					ElementCount: 10,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("array<uint32>:10")},
+		},
+		{
+			symName: "vector_without_size_constraint",
+			symType: analysis.Type{
+				Kind: analysis.VectorType,
+				Vector: &analysis.VectorTypeInfo{
+					ElementType: analysis.Type{
+						Kind: analysis.PrimitiveType,
+						Primitive: &analysis.PrimitiveTypeInfo{
+							Subtype: analysis.Uint32,
+						},
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("vector<uint32>")},
+		},
+		{
+			symName: "nullable_vector_with_size_constraint",
+			symType: analysis.Type{
+				Kind: analysis.VectorType,
+				Vector: &analysis.VectorTypeInfo{
+					ElementType: analysis.Type{
+						Kind: analysis.PrimitiveType,
+						Primitive: &analysis.PrimitiveTypeInfo{
+							Subtype: analysis.Uint32,
+						},
+					},
+					ElementCount: &elementCount,
+					Nullable:     true,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("vector<uint32>:10?")},
+		},
+		{
+			symName: "nullable_string_without_size_constraint",
+			symType: analysis.Type{
+				Kind: analysis.StringType,
+				String: &analysis.StringTypeInfo{
+					Nullable: true,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("string?")},
+		},
+		{
+			symName: "string_with_size_constraint",
+			symType: analysis.Type{
+				Kind: analysis.StringType,
+				String: &analysis.StringTypeInfo{
+					ElementCount: &elementCount,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("string:10")},
+		},
+		{
+			symName: "handle",
+			symType: analysis.Type{
+				Kind: analysis.HandleType,
+				Handle: &analysis.HandleTypeInfo{
+					Subtype:  analysis.Vmo,
+					Nullable: true,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("handle<vmo>?")},
+		},
+		{
+			symName: "request",
+			symType: analysis.Type{
+				Kind: analysis.RequestType,
+				Request: &analysis.RequestTypeInfo{
+					Subtype: "File",
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("request<File>")},
+		},
+		{
+			symName: "primitive_bool",
+			symType: analysis.Type{
+				Kind: analysis.PrimitiveType,
+				Primitive: &analysis.PrimitiveTypeInfo{
+					Subtype: analysis.Bool,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("bool")},
+		},
+		{
+			symName: "primitive_uint64",
+			symType: analysis.Type{
+				Kind: analysis.PrimitiveType,
+				Primitive: &analysis.PrimitiveTypeInfo{
+					Subtype: analysis.Uint64,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("uint64")},
+		},
+		{
+			symName: "bits_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyBits",
+					Kind:   analysis.BitsKind,
+					Bits: &analysis.BitsTypeInfo{
+						Type: analysis.Type{
+							Kind: analysis.PrimitiveType,
+							Primitive: &analysis.PrimitiveTypeInfo{
+								Subtype: analysis.Uint32,
+							},
+						},
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("bits fuchsia.test/MyBits: uint32")},
+		},
+		{
+			symName: "enum_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyEnum",
+					Kind:   analysis.EnumKind,
+					Enum: &analysis.EnumTypeInfo{
+						Type: "string",
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("enum fuchsia.test/MyEnum: string")},
+		},
+		{
+			symName: "const_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyConst",
+					Kind:   analysis.ConstKind,
+					Const: &analysis.ConstTypeInfo{
+						Type: analysis.Type{
+							Kind:   analysis.StringType,
+							String: &analysis.StringTypeInfo{},
+						},
+						Value: "hello, world!",
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("const string fuchsia.test/MyConst = `hello, world!`")},
+		},
+		{
+			symName: "protocol_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyProtocol",
+					Kind:   analysis.ProtocolKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("protocol fuchsia.test/MyProtocol")},
+		},
+		{
+			symName: "service_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyService",
+					Kind:   analysis.ServiceKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("service fuchsia.test/MyService")},
+		},
+		{
+			symName: "struct_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyStruct",
+					Kind:   analysis.StructKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("struct fuchsia.test/MyStruct")},
+		},
+		{
+			symName: "table_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyTable",
+					Kind:   analysis.TableKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("table fuchsia.test/MyTable")},
+		},
+		{
+			symName: "union_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyUnion",
+					Kind:   analysis.UnionKind,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("union fuchsia.test/MyUnion")},
+		},
+		{
+			symName: "type_alias_decl",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/AliasToVectorOfBools",
+					Kind:   analysis.TypeAliasKind,
+					TypeAlias: &analysis.TypeAliasTypeInfo{
+						Type: analysis.Type{
+							Kind: analysis.VectorType,
+							Vector: &analysis.VectorTypeInfo{
+								ElementType: analysis.Type{
+									Kind: analysis.PrimitiveType,
+									Primitive: &analysis.PrimitiveTypeInfo{
+										Subtype: analysis.Bool,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("fuchsia.test/AliasToVectorOfBools: alias to vector<bool>")},
+		},
+		{
+			symName: "nullable_struct_param",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl:     false,
+					Identifier: "fuchsia.test/MyStruct",
+					Nullable:   true,
+				},
+			},
+			hoverText: []lsp.MarkedString{newFIDLString("fuchsia.test/MyStruct?")},
+		},
+		{
+			symName: "type_with_attributes",
+			symType: analysis.Type{
+				Kind: analysis.IdentifierType,
+				Identifier: &analysis.IdentifierTypeInfo{
+					IsDecl: true,
+					Name:   "fuchsia.test/MyProtocol",
+					Kind:   analysis.ProtocolKind,
+				},
+				Attrs: []analysis.Attribute{
+					{
+						Name:  "Doc",
+						Value: "Example doc comments on MyProtocol",
+					},
+					{Name: "Transitional"},
+					{
+						Name:  "OtherAttribute",
+						Value: "AttributeValue",
+					},
+				},
+			},
+			hoverText: []lsp.MarkedString{
+				lsp.RawMarkedString(`Example doc comments on MyProtocol`),
+				newFIDLString(`[Transitional, OtherAttribute = "AttributeValue"]`),
+				newFIDLString(`protocol fuchsia.test/MyProtocol`),
+			},
+		},
+	}
+
+	for _, ex := range cases {
+		hoverText, err := symbolTypeToMarkedStrings(ex.symName, ex.symType)
+		if err != nil {
+			t.Errorf("could not get hover text for symbol `%s`: %s", ex.symName, err)
+			continue
+		}
+		if len(hoverText) != len(ex.hoverText) {
+			t.Errorf(
+				"incorrect number of marked strings for symbol `%s`: expected %d, got %d",
+				ex.symName,
+				len(ex.hoverText),
+				len(hoverText),
+			)
+			continue
+		}
+		for i, expMarkedString := range ex.hoverText {
+			if hoverText[i] != expMarkedString {
+				t.Errorf(
+					"incorrect hoverText for symbol `%s`: expected %v, got %v",
+					ex.symName,
+					expMarkedString,
+					hoverText[i],
+				)
+			}
+		}
+	}
+}
+
+func TestHoverInvalidType(t *testing.T) {
+	_, err := symbolTypeToMarkedStrings(
+		"invalid_type",
+		analysis.Type{
+			Kind: analysis.PrimitiveType,
+			Array: &analysis.ArrayTypeInfo{
+				ElementType: analysis.Type{
+					Kind: analysis.PrimitiveType,
+					Primitive: &analysis.PrimitiveTypeInfo{
+						Subtype: analysis.Uint64,
+					},
+				},
+				ElementCount: 255,
+			},
+		},
+	)
+	if err == nil {
+		t.Errorf("expect error for hover text on invalid type")
+	}
+}
diff --git a/fidl-lsp/langserver/links.go b/fidl-lsp/langserver/links.go
new file mode 100644
index 0000000..378bff8
--- /dev/null
+++ b/fidl-lsp/langserver/links.go
@@ -0,0 +1,42 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"fmt"
+
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/state"
+)
+
+func (h *LangHandler) handleDocumentLinks(params documentLinkParams) ([]documentLink, error) {
+	file, err := h.fs.File(state.FileID(params.TextDocument.URI))
+	if err != nil {
+		return nil, fmt.Errorf("could not find file `%s`", params.TextDocument.URI)
+	}
+	imports := state.ParsePlatformImportsMatch(file)
+	links := []documentLink{}
+	for _, m := range imports {
+		links = append(links, documentLink{
+			Range: lsp.Range{
+				Start: lsp.Position{
+					Line:      m.Range.Start.Line,
+					Character: m.Range.Start.Character,
+				},
+				End: lsp.Position{
+					Line:      m.Range.End.Line,
+					Character: m.Range.End.Character,
+				},
+			},
+			Target:  lsp.DocumentURI(h.cfg.FormatRefURI(m.Lib)),
+			Tooltip: fmt.Sprintf("Fuchsia SDK Docs for library %s", m.Lib.FullyQualifiedName()),
+		})
+	}
+
+	return links, nil
+}
+
+// TODO: fxr/ --> fxrev.dev/, fxb/ --> fxbug.dev/?
diff --git a/fidl-lsp/langserver/links_test.go b/fidl-lsp/langserver/links_test.go
new file mode 100644
index 0000000..2c65e7a
--- /dev/null
+++ b/fidl-lsp/langserver/links_test.go
@@ -0,0 +1,72 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/state"
+)
+
+func TestLinks(t *testing.T) {
+	fileText := `
+library test;
+
+using fuchsia.io;
+using non.platform.import;
+`
+	lines := strings.SplitAfter(fileText, "\n")
+
+	handler := NewHandlerWithFiles(TestFile{
+		Name: "test.fidl",
+		Text: fileText,
+	})
+
+	links, err := handler.handleDocumentLinks(documentLinkParams{
+		TextDocument: lsp.TextDocumentIdentifier{
+			URI: lsp.DocumentURI("test.fidl"),
+		},
+	})
+	if err != nil {
+		t.Fatalf("failed to get document links: %s", err)
+	}
+	expLinks := map[string]lsp.DocumentURI{
+		"fuchsia.io": lsp.DocumentURI("https://fuchsia.dev/reference/fidl/fuchsia.io"),
+	}
+	if len(links) != len(expLinks) {
+		t.Fatalf("incorrect number of links; expected 1, actual %d", len(links))
+	}
+	for expSpan, expLink := range expLinks {
+		found := false
+		for _, link := range links {
+			start, err := state.OffsetInFile(
+				lines,
+				state.Position{Line: link.Range.Start.Line, Character: link.Range.Start.Character},
+			)
+			if err != nil {
+				t.Fatalf("could not get offset of link in file: %s", err)
+			}
+			end, err := state.OffsetInFile(
+				lines,
+				state.Position{Line: link.Range.End.Line, Character: link.Range.End.Character},
+			)
+			if err != nil {
+				t.Fatalf("could not get offset of link in file: %s", err)
+			}
+			span := fileText[start:end]
+			if span == expSpan && link.Target == expLink {
+				found = true
+				break
+			}
+		}
+		// Check if it's in links
+		if !found {
+			t.Fatalf("missing expected link `%s` on span `%s`", expLink, expSpan)
+		}
+	}
+}
diff --git a/fidl-lsp/langserver/lsp.go b/fidl-lsp/langserver/lsp.go
new file mode 100644
index 0000000..07e0f55
--- /dev/null
+++ b/fidl-lsp/langserver/lsp.go
@@ -0,0 +1,67 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import "github.com/sourcegraph/go-lsp"
+
+// `go-lsp` does not include all the necessary types for an LSP server. For
+// example, the types associated with the "textDocument/documentLink" language
+// feature are missing. This file includes those missing definitions.
+
+type serverCapabilities struct {
+	TextDocumentSync                 *lsp.TextDocumentSyncOptionsOrKind   `json:"textDocumentSync,omitempty"`
+	HoverProvider                    bool                                 `json:"hoverProvider,omitempty"`
+	CompletionProvider               *lsp.CompletionOptions               `json:"completionProvider,omitempty"`
+	SignatureHelpProvider            *lsp.SignatureHelpOptions            `json:"signatureHelpProvider,omitempty"`
+	DefinitionProvider               bool                                 `json:"definitionProvider,omitempty"`
+	TypeDefinitionProvider           bool                                 `json:"typeDefinitionProvider,omitempty"`
+	ReferencesProvider               bool                                 `json:"referencesProvider,omitempty"`
+	DocumentHighlightProvider        bool                                 `json:"documentHighlightProvider,omitempty"`
+	DocumentSymbolProvider           bool                                 `json:"documentSymbolProvider,omitempty"`
+	WorkspaceSymbolProvider          bool                                 `json:"workspaceSymbolProvider,omitempty"`
+	ImplementationProvider           bool                                 `json:"implementationProvider,omitempty"`
+	CodeActionProvider               bool                                 `json:"codeActionProvider,omitempty"`
+	CodeLensProvider                 *lsp.CodeLensOptions                 `json:"codeLensProvider,omitempty"`
+	DocumentFormattingProvider       bool                                 `json:"documentFormattingProvider,omitempty"`
+	DocumentRangeFormattingProvider  bool                                 `json:"documentRangeFormattingProvider,omitempty"`
+	DocumentOnTypeFormattingProvider *lsp.DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"`
+	DocumentLinkProvider             *documentLinkOptions                 `json:"documentLinkProvider,omitempty"`
+	RenameProvider                   bool                                 `json:"renameProvider,omitempty"`
+	ExecuteCommandProvider           *lsp.ExecuteCommandOptions           `json:"executeCommandProvider,omitempty"`
+	SemanticHighlighting             *lsp.SemanticHighlightingOptions     `json:"semanticHighlighting,omitempty"`
+
+	// XWorkspaceReferencesProvider indicates the server provides support for
+	// xworkspace/references. This is a Sourcegraph extension.
+	XWorkspaceReferencesProvider bool `json:"xworkspaceReferencesProvider,omitempty"`
+
+	// XDefinitionProvider indicates the server provides support for
+	// textDocument/xdefinition. This is a Sourcegraph extension.
+	XDefinitionProvider bool `json:"xdefinitionProvider,omitempty"`
+
+	// XWorkspaceSymbolByProperties indicates the server provides support for
+	// querying symbols by properties with WorkspaceSymbolParams.symbol. This
+	// is a Sourcegraph extension.
+	XWorkspaceSymbolByProperties bool `json:"xworkspaceSymbolByProperties,omitempty"`
+
+	Experimental interface{} `json:"experimental,omitempty"`
+}
+
+type initializeResult struct {
+	Capabilities serverCapabilities `json:"capabilities,omitempty"`
+}
+
+type documentLinkOptions struct {
+	ResolveProvider bool `json:"resolveProvider,omitempty"`
+}
+
+type documentLinkParams struct {
+	TextDocument lsp.TextDocumentIdentifier `json:"textDocument"`
+}
+
+type documentLink struct {
+	Range   lsp.Range       `json:"range"`
+	Target  lsp.DocumentURI `json:"target"`
+	Tooltip string          `json:"tooltip"`
+}
diff --git a/fidl-lsp/langserver/references.go b/fidl-lsp/langserver/references.go
new file mode 100644
index 0000000..26e41a2
--- /dev/null
+++ b/fidl-lsp/langserver/references.go
@@ -0,0 +1,45 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"github.com/sourcegraph/go-lsp"
+
+	"fidl-lsp/state"
+)
+
+func (h *LangHandler) handleReferences(params lsp.TextDocumentPositionParams) ([]lsp.Location, error) {
+	sym, err := h.fs.SymbolAtPos(
+		state.FileID(params.TextDocument.URI),
+		state.Position{Line: params.Position.Line, Character: params.Position.Character},
+	)
+	if err != nil {
+		h.log.Printf(
+			"could not find symbol at position `%#v` in document `%s`\n",
+			params.Position,
+			params.TextDocument.URI,
+		)
+		return []lsp.Location{}, nil
+	}
+
+	locs, err := h.analyzer.ReferencesToSymbol(h.fs, sym)
+	if err != nil {
+		h.log.Printf("error on definition: %s\n", err)
+		return []lsp.Location{}, nil
+	}
+
+	// Convert state.Locations --> lsp.Locations
+	lspLocs := make([]lsp.Location, len(locs))
+	for i, loc := range locs {
+		lspLocs[i] = lsp.Location{
+			URI: lsp.DocumentURI(loc.FileID),
+			Range: lsp.Range{
+				Start: lsp.Position{Line: loc.Range.Start.Line, Character: loc.Range.Start.Character},
+				End:   lsp.Position{Line: loc.Range.End.Line, Character: loc.Range.End.Character},
+			},
+		}
+	}
+	return lspLocs, nil
+}
diff --git a/fidl-lsp/langserver/testutils.go b/fidl-lsp/langserver/testutils.go
new file mode 100644
index 0000000..43ce4c5
--- /dev/null
+++ b/fidl-lsp/langserver/testutils.go
@@ -0,0 +1,49 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package langserver
+
+import (
+	"log"
+	"os"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/state"
+)
+
+var (
+	fuchsiaDir     = os.Getenv("FUCHSIA_DIR")
+	toolsPath      = fuchsiaDir + "/out/default.zircon/tools/"
+	fidlcPath      = toolsPath + "fidlc"
+	fidlLintPath   = toolsPath + "fidl-lint"
+	fidlFormatPath = toolsPath + "fidl-format"
+)
+
+type TestFile struct {
+	Name string
+	Text string
+}
+
+func NewHandlerWithFiles(files ...TestFile) *LangHandler {
+	handler := NewLangHandler(
+		NewDefaultConfig(),
+		log.New(os.Stderr, "[LSP Test] ", log.Lshortfile),
+		analysis.NewAnalyzer(
+			analysis.Config{
+				BuildRootDir:   fuchsiaDir,
+				FidlcPath:      fidlcPath,
+				FidlLintPath:   fidlLintPath,
+				FidlFormatPath: fidlFormatPath,
+			},
+			analysis.CompiledLibraries{},
+		),
+	)
+
+	for _, file := range files {
+		handler.fs.NewFile(state.FileID(file.Name), file.Text)
+		handler.analyzer.Analyze(handler.fs, state.FileID(file.Name))
+	}
+
+	return handler
+}
diff --git a/fidl-lsp/main.go b/fidl-lsp/main.go
new file mode 100644
index 0000000..8a387bd
--- /dev/null
+++ b/fidl-lsp/main.go
@@ -0,0 +1,82 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+	"context"
+	"log"
+	"os"
+
+	"github.com/sourcegraph/jsonrpc2"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/langserver"
+)
+
+var (
+	fuchsiaDir      = os.Getenv("FUCHSIA_DIR")
+	outDir          = fuchsiaDir + "/out/default/"
+	fidlProjectPath = outDir + "fidl_project.json"
+	toolsPath       = fuchsiaDir + "/out/default.zircon/tools/"
+	fidlcPath       = toolsPath + "fidlc"
+	fidlLintPath    = toolsPath + "fidl-lint"
+	fidlFormatPath  = toolsPath + "fidl-format"
+)
+
+type stdrwc struct{}
+
+func (stdrwc) Read(p []byte) (int, error) {
+	return os.Stdin.Read(p)
+}
+
+func (stdrwc) Write(p []byte) (int, error) {
+	return os.Stdout.Write(p)
+}
+
+func (stdrwc) Close() error {
+	if err := os.Stdin.Close(); err != nil {
+		return err
+	}
+	return os.Stdout.Close()
+}
+
+func main() {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	trace := log.New(os.Stderr, "[LSP] ", log.Lshortfile)
+	logOpt := jsonrpc2.LogMessages(trace)
+
+	// fidl_project.json is generated by running
+	// `python3 scripts/gen_fidl_project.py path/to/fidl_project.json`
+	fidlProject, err := analysis.LoadFidlProject(fidlProjectPath)
+	if err != nil {
+		trace.Fatalf("Failed to parse fidl_project.json at `%s`: %s\n", fidlProjectPath, err)
+	}
+	analyzer := analysis.NewAnalyzer(
+		analysis.Config{
+			BuildRootDir:   fuchsiaDir,
+			FidlcPath:      fidlcPath,
+			FidlLintPath:   fidlLintPath,
+			FidlFormatPath: fidlFormatPath,
+		},
+		fidlProject,
+	)
+
+	handler := langserver.NewLangHandler(langserver.NewDefaultConfig(), trace, analyzer)
+	server := langserver.LspServer{handler}
+
+	// Setup a new Conn over stdio
+	trace.Println("setting up a new connection")
+	conn := jsonrpc2.NewConn(
+		ctx,
+		jsonrpc2.NewBufferedStream(stdrwc{}, jsonrpc2.VSCodeObjectCodec{}),
+		server,
+		logOpt,
+	)
+	defer conn.Close()
+
+	<-conn.DisconnectNotify()
+}
diff --git a/fidl-lsp/scripts/copy_fidlgen_common.py b/fidl-lsp/scripts/copy_fidlgen_common.py
new file mode 100644
index 0000000..b59abfa
--- /dev/null
+++ b/fidl-lsp/scripts/copy_fidlgen_common.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""
+copy fidlgen's fidl/compiler/backend/common/identifiers.go from fuchsia.git
+
+This script copies the `identifiers.go` file from the fildgen `common` package
+in fuchsia.git. This is used to share common FIDL library identifier code with
+the fidlgens. It should be run on build to maintain freshness.
+
+TODO: it would be better if we could just directly import the `common` package.
+
+The script assumes that it is run from the fidl-lsp root
+directory.
+"""
+import os
+from pathlib import Path
+from shutil import copyfile
+
+FUCHSIA_DIR = os.getenv('FUCHSIA_DIR')
+FIDLGEN_COMMON_PATH = 'garnet/go/src/fidl/compiler/backend/common/identifiers.go'
+
+
+def copy_fidlgen_common():
+    project_root = path = Path.cwd()
+    assert project_root.name == 'fidl-lsp', 'Script should be run from fidl-lsp root directory'
+    Path(f'{project_root}/third_party/common').mkdir(exist_ok=True)
+    copyfile(f'{FUCHSIA_DIR}/{FIDLGEN_COMMON_PATH}', f'{project_root}/third_party/common/identifiers.go')
+
+
+if __name__ == '__main__':
+    copy_fidlgen_common()
diff --git a/fidl-lsp/scripts/gen_fidl_project.py b/fidl-lsp/scripts/gen_fidl_project.py
new file mode 100644
index 0000000..ebd2bd3
--- /dev/null
+++ b/fidl-lsp/scripts/gen_fidl_project.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""
+generate fidl_project.json file declaring FIDL libraries
+
+This script reads the all_fidl_generated.json file which contains all fidlgen- &
+fidlc-generated output, and generates a fidl_project.json file which declares
+all FIDL libraries along with their constituent files, dependencies, and build
+artifacts (JSON IR and bindings). This is for use in the FIDL Language Server,
+which uses fidl_project to do dependency resolution.
+
+This script makes the assumption that all FIDL library names are unique, which
+is currently not the case. However, the exceptions are mainly test libraries, so
+for now this is left unaddressed.
+"""
+import glob
+import json
+import os
+import re
+import sys
+from pathlib import Path
+
+FUCHSIA_DIR = os.getenv('FUCHSIA_DIR')
+ALL_FIDL_GENERATED_PATH = 'out/default/all_fidl_generated.json'
+
+# schema:
+# map<string, Library>
+# where string is library name e.g. 'fuchsia.mem'
+# and Library is
+# {
+#     "files": []string,
+#     "json": string,
+#     "deps": []string,
+#     "bindings": {
+#         "hlcpp": {},
+#         "llcpp": {},
+#         "rust": {},
+#         "go": {},
+#         "dart": {},
+#         ...
+#     }
+# }
+
+
+def find_files(artifact):
+    library_name = artifact['library']
+    pattern = f'^fidling\/gen\/([\w\.\/-]+)\/[\w\-. ]+\.fidl\.json$'
+    result = re.search(pattern, artifact['files'][0])
+    if not result or not result.group(1):
+        return []
+
+    fidl_dir = Path(f'{FUCHSIA_DIR}/{result.group(1)}')
+    globs = [
+        fidl_dir.glob('*.fidl'),
+        fidl_dir.parent.glob('*.fidl'),
+    ]
+
+    files = []
+    for glob in globs:
+        for file in glob:
+            # TODO: read in file
+            # parse `library` decl
+            # check that it matches library name
+            files.append(str(file))
+    return files
+
+
+def find_deps(artifact):
+    library_json = artifact['files'][0]
+    library_json_path = Path(f'{FUCHSIA_DIR}/out/default/{library_json}')
+    with open(library_json_path, 'r') as f:
+        library = json.load(f)
+        deps = library['library_dependencies']
+        deps = [dep['name'] for dep in deps]
+        return deps
+
+
+def gen_fidl_project(fidl_project_path):
+    result = {}
+
+    all_fidl_path = FUCHSIA_DIR + '/' + ALL_FIDL_GENERATED_PATH
+    with open(all_fidl_path, 'r') as f:
+        artifacts = json.load(f)
+
+    print('have read in all_fidl_generated')
+    print(len(artifacts))
+
+    for artifact in artifacts:
+        if artifact['type'] == 'json':
+            result[artifact['library']] = {
+                'json': f"{FUCHSIA_DIR}/out/default/{artifact['files'][0]}",
+                'files': find_files(artifact),
+                'deps': find_deps(artifact),
+                'bindings': {},  # TODO
+            }
+
+    print('writing to {}', fidl_project_path)
+    with open(fidl_project_path, 'w') as f:
+        json.dump(result, f, indent=4, sort_keys=True)
+
+
+if __name__ == '__main__':
+    gen_fidl_project(sys.argv[1])
diff --git a/fidl-lsp/state/fs.go b/fidl-lsp/state/fs.go
new file mode 100644
index 0000000..b6eb790
--- /dev/null
+++ b/fidl-lsp/state/fs.go
@@ -0,0 +1,154 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package state
+
+import (
+	"fmt"
+	"net/url"
+	"strings"
+)
+
+// A FileID is a path to a file open in the client editor.
+//
+// We never assume the "saved-ness" of a file, and maintain all files that are
+// open in the editor in memory. Therefore this path does not necessarily
+// correspond to an absolute filepath; it's just used as an ID to access the
+// in-memory file.
+type FileID string
+
+// FileSystem manages file synchronization between client and server.
+// `files` is an in-memory mapping of URI to file contents of the currently open
+// files on the client.
+type FileSystem struct {
+	// TODO: should this be a map[FileID][]byte instead?
+
+	// `files` is stored as a map of FileIDs to files that are split by line
+	// (but retaining their '\n' characters).
+	// This speeds up the common operation of searching for a symbol at a given
+	// (line, column) in the file as the file is indexed by line.
+	files map[FileID][]string
+
+	// TODO: RWLock for safe concurrency
+}
+
+// Range represents a span of text in a source file.
+type Range struct {
+	Start Position
+	End   Position
+}
+
+// Position represents the location of one character in a source file.
+// Both are zero-indexed, i.e. lines start at 0 and the first character of a
+// line is at index 0.
+type Position struct {
+	Line      int
+	Character int
+}
+
+// Location represents a range in a particular file.
+type Location struct {
+	FileID FileID
+	Range  Range
+}
+
+// Change represents a single diff in a source file.
+// Range is the range that is being changed. RangeLength is the length of the
+// replacement text (0 if the range is being deleted). Text is the text to
+// insert in the source at that range.
+type Change struct {
+	Range      Range
+	NewContent string
+}
+
+// NewFileSystem returns an initialized empty FileSystem.
+func NewFileSystem() *FileSystem {
+	return &FileSystem{
+		files: make(map[FileID][]string),
+	}
+}
+
+// File is a read-only accessor for the in-memory file with ID `path`.
+func (fs *FileSystem) File(path FileID) (string, error) {
+	if lines, ok := fs.files[path]; ok {
+		return strings.Join(lines, ""), nil
+	}
+	return "", fmt.Errorf("file `%s` not in memory", path)
+}
+
+// NFiles returns the number of in-memory files.
+func (fs *FileSystem) NFiles() int {
+	return len(fs.files)
+}
+
+// NewFile copies `text` into the FileSystem's in-memory file system ("opening"
+// the file), indexed by the FileID `path`.
+func (fs *FileSystem) NewFile(path FileID, text string) {
+	fs.files[path] = strings.SplitAfter(text, "\n")
+}
+
+// DeleteFile deletes the FileSystem's in-memory representation of the file
+// indexed by `path`.
+func (fs *FileSystem) DeleteFile(path FileID) {
+	delete(fs.files, path)
+}
+
+// ApplyChanges applies the specified changes to the relevant in-memory file.
+func (fs *FileSystem) ApplyChanges(path FileID, changes []Change) error {
+	for _, change := range changes {
+		start, err := OffsetInFile(fs.files[path], change.Range.Start)
+		if err != nil {
+			return err
+		}
+		end, err := OffsetInFile(fs.files[path], change.Range.End)
+		if err != nil {
+			return err
+		}
+
+		file := strings.Join(fs.files[path], "")
+
+		// Copy file over to new buffer with inserted text change
+		var newFile strings.Builder
+		newFile.WriteString(file[:start])
+		newFile.WriteString(change.NewContent)
+		newFile.WriteString(file[end:])
+		fs.files[path] = strings.SplitAfter(newFile.String(), "\n")
+	}
+	return nil
+}
+
+// OffsetInFile converts a Position (line, character) to a rune offset in
+// `contents`.
+func OffsetInFile(lines []string, pos Position) (uint, error) {
+	var offset uint
+	for line, l := range lines {
+		for col := range l {
+			if line == pos.Line && col == pos.Character {
+				return offset, nil
+			}
+			offset++
+		}
+	}
+	// Check if pos is pointing at the end of the file
+	lastLine := len(lines) - 1
+	if pos.Line == lastLine && pos.Character == len(lines[lastLine]) {
+		return offset, nil
+	}
+	return 0, fmt.Errorf(
+		"position %#v was out of bounds in file with %d lines, %d total chars",
+		pos,
+		len(lines),
+		offset,
+	)
+}
+
+// FileIDToPath converts a FileID, which is in the form of a file schema URI
+// ("file:///"), to an absolute path.
+func FileIDToPath(fileID FileID) (string, error) {
+	fileURI, err := url.Parse(string(fileID))
+	if err != nil {
+		return "", fmt.Errorf("could not parse FileID: %s", err)
+	}
+	return fileURI.Path, nil
+}
diff --git a/fidl-lsp/state/fs_test.go b/fidl-lsp/state/fs_test.go
new file mode 100644
index 0000000..39d3654
--- /dev/null
+++ b/fidl-lsp/state/fs_test.go
@@ -0,0 +1,293 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package state_test
+
+import (
+	"testing"
+
+	"fidl-lsp/state"
+)
+
+func TestFsApplyChangesInsertions(t *testing.T) {
+	fs := state.NewFileSystem()
+
+	fs.NewFile("test.fidl", `
+library example;
+
+struct Foo {
+    uint8 foo;
+};
+`)
+
+	err := fs.ApplyChanges("test.fidl",
+		[]state.Change{
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 1, Character: 10},
+					End:   state.Position{Line: 1, Character: 10},
+				},
+				NewContent: "tended.library.ex",
+			},
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 2, Character: 0},
+					End:   state.Position{Line: 2, Character: 0},
+				},
+				NewContent: "\nconst uint8 INSERTED_CONST = 0;\n",
+			},
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 5, Character: 0},
+					End:   state.Position{Line: 5, Character: 0},
+				},
+				NewContent: "/// Inserted doc comment\n",
+			},
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 7, Character: 14},
+					End:   state.Position{Line: 7, Character: 14},
+				},
+				NewContent: "\n    string inserted_struct_member;",
+			},
+		},
+	)
+	if err != nil {
+		t.Errorf("error on applying changes: %s", err)
+	}
+
+	expFile := `
+library extended.library.example;
+
+const uint8 INSERTED_CONST = 0;
+
+/// Inserted doc comment
+struct Foo {
+    uint8 foo;
+    string inserted_struct_member;
+};
+`
+	file, err := fs.File("test.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	if file != expFile {
+		t.Errorf(
+			"file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+			expFile,
+			file,
+		)
+	}
+}
+
+func TestFsApplyChangesDeletions(t *testing.T) {
+	fs := state.NewFileSystem()
+
+	fs.NewFile("test.fidl", `
+library example;
+
+struct ToBeDeleted {
+    uint8 foo;
+};
+`)
+
+	err := fs.ApplyChanges("test.fidl",
+		[]state.Change{
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 3, Character: 0},
+					End:   state.Position{Line: 6, Character: 0},
+				},
+				NewContent: "",
+			},
+		},
+	)
+	if err != nil {
+		t.Errorf("error on applying changes: %s", err)
+	}
+
+	expFile := `
+library example;
+
+`
+	file, err := fs.File("test.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	if file != expFile {
+		t.Errorf(
+			"file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+			expFile,
+			file,
+		)
+	}
+}
+
+func TestFsApplyChangesToBeginningOfFile(t *testing.T) {
+	fs := state.NewFileSystem()
+
+	fs.NewFile(
+		"test.fidl",
+		`library example;`,
+	)
+
+	err := fs.ApplyChanges("test.fidl",
+		[]state.Change{
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 0, Character: 0},
+					End:   state.Position{Line: 0, Character: 0},
+				},
+				NewContent: "// Prepended comment\n",
+			},
+		},
+	)
+	if err != nil {
+		t.Errorf("error on applying changes: %s", err)
+	}
+
+	expFile := `// Prepended comment
+library example;`
+	file, err := fs.File("test.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	if file != expFile {
+		t.Errorf(
+			"file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+			expFile,
+			file,
+		)
+	}
+
+	err = fs.ApplyChanges("test.fidl",
+		[]state.Change{
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 0, Character: 0},
+					End:   state.Position{Line: 1, Character: 0},
+				},
+				NewContent: "",
+			},
+		},
+	)
+	if err != nil {
+		t.Errorf("error on applying changes: %s", err)
+	}
+
+	expFile = `library example;`
+	file, err = fs.File("test.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	if file != expFile {
+		t.Errorf(
+			"file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+			expFile,
+			file,
+		)
+	}
+}
+
+func TestFsApplyChangesToEndOfFile(t *testing.T) {
+	fs := state.NewFileSystem()
+
+	fs.NewFile("test.fidl", `
+library example;
+`)
+
+	err := fs.ApplyChanges("test.fidl",
+		[]state.Change{
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 1, Character: 16},
+					End:   state.Position{Line: 1, Character: 16},
+				},
+				NewContent: "\n// Appended comment",
+			},
+		},
+	)
+	if err != nil {
+		t.Errorf("error on applying changes: %s", err)
+	}
+
+	expFile := `
+library example;
+// Appended comment
+`
+	file, err := fs.File("test.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	if file != expFile {
+		t.Errorf(
+			"file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+			expFile,
+			file,
+		)
+	}
+
+	fs.ApplyChanges("test.fidl",
+		[]state.Change{
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 1, Character: 16},
+					End:   state.Position{Line: 2, Character: 19},
+				},
+				NewContent: "",
+			},
+		},
+	)
+
+	expFile = `
+library example;
+`
+	file, err = fs.File("test.fidl")
+	if err != nil {
+		t.Error(err)
+	}
+	if file != expFile {
+		t.Errorf(
+			"file changes were not applied correctly. expected:\n%s\nfound:\n%s\n",
+			expFile,
+			file,
+		)
+	}
+}
+
+func TestFsApplyChangesOutsideFileRange(t *testing.T) {
+	fs := state.NewFileSystem()
+
+	fs.NewFile("test.fidl", `library example;`)
+
+	err := fs.ApplyChanges("test.fidl",
+		[]state.Change{
+			{
+				Range: state.Range{
+					Start: state.Position{Line: -1, Character: -1},
+					End:   state.Position{Line: 0, Character: 0},
+				},
+				NewContent: "inserted text",
+			},
+		},
+	)
+	if err == nil {
+		t.Errorf("expect error on applying change out of file bounds")
+	}
+
+	err = fs.ApplyChanges("test.fidl",
+		[]state.Change{
+			{
+				Range: state.Range{
+					Start: state.Position{Line: 1, Character: 0},
+					End:   state.Position{Line: 1, Character: 0},
+				},
+				NewContent: "inserted text",
+			},
+		},
+	)
+	if err == nil {
+		t.Errorf("expect error on applying change out of file bounds")
+	}
+}
diff --git a/fidl-lsp/state/parse.go b/fidl-lsp/state/parse.go
new file mode 100644
index 0000000..f91f9b3
--- /dev/null
+++ b/fidl-lsp/state/parse.go
@@ -0,0 +1,169 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package state
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+
+	fidlcommon "fidl-lsp/third_party/common"
+)
+
+var (
+	// https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/language.md#identifiers
+	fidlIdentifierPattern = `[a-zA-Z](?:[a-zA-Z0-9_]*[a-zA-Z0-9])?`
+
+	// Although "library" can be used anywhere (e.g. as a type name), this regex
+	// is robust because the the library declaration must appear at the top of
+	// the file (only comments and whitespace can precede it).
+	libraryRegexp = regexp.MustCompile(`` +
+		`^(?:\s*//[^\n]*\n)*\s*` +
+		`library\s+(` +
+		fidlIdentifierPattern +
+		`(?:\.` + fidlIdentifierPattern + `)*` +
+		`)\s*;`)
+
+	// TODO: make this agnostic to platform
+
+	// Although "using" can be used anywhere (e.g. as a type name), this regex
+	// is robust because it only matches imports of platform libraries (ones
+	// that start with fidl, fuchsia, or test) and the special library zx. The
+	// only problem is it can match commented imports, so we have to check for
+	// this after matching.
+	platformImportRegexp = regexp.MustCompile(`` +
+		`\busing\s+(` +
+		`zx|` +
+		`(?:fidl|fuchsia|test)` +
+		`(?:\.` + fidlIdentifierPattern + `)+` +
+		`)` +
+		`(?:\s+as\s+` + fidlIdentifierPattern + `)?` +
+		`\s*;`)
+)
+
+// LibraryMatch is the result of ParseLibraryMatch and ParsePlatformImportsMatch
+// and represents the location and value of a `library` or `using` declaration
+// in a FIDL file.
+type LibraryMatch struct {
+	Lib   fidlcommon.LibraryName
+	Range Range
+}
+
+// TODO: we should keep track of URI <-> fidlLibrary mapping, so we don't have
+// to parse for `library` name each time
+
+// LibraryOfFile extracts the name of the FIDL library of the provided FIDL
+// file, by extracting its `library` declaration.
+func LibraryOfFile(file string) (fidlcommon.LibraryName, error) {
+	fidlLib, ok := ParseLibraryMatch(file)
+	if !ok {
+		return fidlcommon.LibraryName{}, fmt.Errorf("Could not find library declaration in file")
+	}
+	return fidlLib.Lib, nil
+}
+
+// ParseLibraryMatch extracts with regex the `library __;` declaration from the
+// FIDL file passed in, if there is one, or returns false if not.
+func ParseLibraryMatch(fidl string) (LibraryMatch, bool) {
+	m := libraryRegexp.FindStringSubmatchIndex(fidl)
+	if m == nil {
+		return LibraryMatch{}, false
+	}
+	return toLibraryMatch(fidl, m[2], m[3]), true
+}
+
+// ParsePlatformImportsMatch extracts with regex the `using __;` import
+// declarations in the FIDL file passed in.
+func ParsePlatformImportsMatch(fidl string) []LibraryMatch {
+	var libs []LibraryMatch
+	for _, m := range platformImportRegexp.FindAllStringSubmatchIndex(fidl, -1) {
+		// See the comment in parsePlatformImports.
+		i := strings.LastIndexByte(fidl[:m[2]], '\n')
+		if i == -1 || !strings.Contains(fidl[i:m[2]], "//") {
+			libs = append(libs, toLibraryMatch(fidl, m[2], m[3]))
+		}
+	}
+	return libs
+}
+
+func toLibraryMatch(fidl string, start, end int) LibraryMatch {
+	return LibraryMatch{
+		Lib: fidlcommon.MustReadLibraryName(fidl[start:end]),
+		Range: Range{
+			Start: Position{
+				Line:      strings.Count(fidl[:start], "\n"),
+				Character: start - strings.LastIndexByte(fidl[:start], '\n') - 1,
+			},
+			End: Position{
+				Line:      strings.Count(fidl[:end], "\n"),
+				Character: end - strings.LastIndexByte(fidl[:end], '\n') - 1,
+			},
+		},
+	}
+}
+
+// https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/language.md#identifiers
+//
+// This also includes a '.' as long as it is not initial or trailing, so we can
+// capture fully-qualified names and library names.
+var identRegexp = regexp.MustCompile(`\b[a-zA-Z](?:[a-zA-Z0-9_.]*[a-zA-Z0-9])?\b`)
+
+// Symbol represents a FIDL identifier.
+// It could be a fully-qualified name, a local declaration name, a library name,
+// etc.
+type Symbol struct {
+	Name     string
+	Location Location
+}
+
+// SymbolAtPos returns the FIDL identifier that is at `pos` in `file`.
+func (fs *FileSystem) SymbolAtPos(path FileID, pos Position) (Symbol, error) {
+	lines, ok := fs.files[path]
+	if !ok {
+		return Symbol{}, fmt.Errorf("could not find file `%s`", path)
+	}
+
+	if pos.Line >= len(lines) {
+		return Symbol{}, fmt.Errorf("position %#v in file `%s` is out of bounds", pos, path)
+	}
+	line := lines[pos.Line]
+	tokens := identRegexp.FindAllStringIndex(line, -1)
+	if len(tokens) == 0 {
+		return Symbol{}, fmt.Errorf("could not find any symbols in line %d in file `%s`", pos.Line, path)
+	}
+
+	var name string
+	r := Range{
+		Start: Position{Line: pos.Line},
+		End:   Position{Line: pos.Line},
+	}
+	for _, token := range tokens {
+		if pos.Character >= token[0] &&
+			pos.Character <= token[1] {
+			name = line[token[0]:token[1]]
+			r.Start.Character = token[0]
+			r.End.Character = token[1]
+			break
+		}
+	}
+	if name == "" {
+		return Symbol{}, fmt.Errorf("could not find symbol at pos %#v in file `%s`", pos, path)
+	}
+
+	// TODO: could exclude all FIDL keywords, to save work
+
+	// TODO: check that the symbol is not part of a comment: if the line
+	// contains "//" and the symbol is after the "//", then don't return the
+	// symbol. "//" could also be part of a string const, but that also wouldn't
+	// be an identifer.
+
+	return Symbol{
+		Name: name,
+		Location: Location{
+			FileID: path,
+			Range:  r,
+		},
+	}, nil
+}
diff --git a/fidl-lsp/state/parse_test.go b/fidl-lsp/state/parse_test.go
new file mode 100644
index 0000000..6e32a0d
--- /dev/null
+++ b/fidl-lsp/state/parse_test.go
@@ -0,0 +1,287 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package state_test
+
+import (
+	"testing"
+
+	fidlcommon "fidl-lsp/third_party/common"
+
+	"fidl-lsp/state"
+)
+
+func TestLibraryOfFile(t *testing.T) {
+	library, err := state.LibraryOfFile(`library example;`)
+	if err != nil {
+		t.Errorf("error getting library of file: %s", err)
+	}
+	expLibrary := "example"
+	if library.FullyQualifiedName() != expLibrary {
+		t.Errorf("expected library `%s`, got `%s`", expLibrary, library)
+	}
+
+	library, err = state.LibraryOfFile(`
+/// Doc comments
+library fuchsia.foo.bar;
+struct S {};
+`)
+	if err != nil {
+		t.Errorf("error getting library of file: %s", err)
+	}
+	expLibrary = "fuchsia.foo.bar"
+	if library.FullyQualifiedName() != expLibrary {
+		t.Errorf("expected library `%s`, got `%s`", expLibrary, library)
+	}
+
+	library, err = state.LibraryOfFile(`library .invalid.name;`)
+	if err == nil {
+		t.Error("expect error getting library of file")
+	}
+
+	library, err = state.LibraryOfFile(`library exampl // Missing semicolon`)
+	if err == nil {
+		t.Error("expect error getting library of file")
+	}
+}
+
+func TestParseLibraryMatch(t *testing.T) {
+	library, ok := state.ParseLibraryMatch(`
+library example;
+//      ~~~~~~~ expected range
+`)
+	if !ok {
+		t.Errorf("error getting library of file")
+	}
+	expLibrary := state.LibraryMatch{
+		Lib: fidlcommon.MustReadLibraryName("example"),
+		Range: state.Range{
+			Start: state.Position{Line: 1, Character: 8},
+			End:   state.Position{Line: 1, Character: 15},
+		},
+	}
+	if library != expLibrary {
+		t.Errorf("expected library `%v`, got `%v`", expLibrary, library)
+	}
+
+	library, ok = state.ParseLibraryMatch(`
+/// Doc comments
+library fuchsia.foo.bar;
+//      ~~~~~~~~~~~~~~~ expected range
+struct S {};
+`)
+	if !ok {
+		t.Errorf("error getting library of file")
+	}
+	expLibrary = state.LibraryMatch{
+		Lib: fidlcommon.MustReadLibraryName("fuchsia.foo.bar"),
+		Range: state.Range{
+			Start: state.Position{Line: 2, Character: 8},
+			End:   state.Position{Line: 2, Character: 23},
+		},
+	}
+	if library != expLibrary {
+		t.Errorf("expected library `%v`, got `%v`", expLibrary, library)
+	}
+
+	library, ok = state.ParseLibraryMatch(`library .invalid.name;`)
+	if ok {
+		t.Error("expect error getting library of file")
+	}
+
+	library, ok = state.ParseLibraryMatch(`library exampl // Missing semicolon`)
+	if ok {
+		t.Error("expect error getting library of file")
+	}
+}
+
+func TestParsePlatformImportsMatch(t *testing.T) {
+	imports := state.ParsePlatformImportsMatch(`
+library example;
+using fuchsia.foo.bar;
+using fuchsia.baz.qux;
+using non.platform.import;
+using TypeAlias = uint8;
+`)
+
+	expImports := []state.LibraryMatch{
+		{
+			Lib: fidlcommon.MustReadLibraryName("fuchsia.foo.bar"),
+			Range: state.Range{
+				Start: state.Position{Line: 2, Character: 6},
+				End:   state.Position{Line: 2, Character: 21},
+			},
+		},
+		{
+			Lib: fidlcommon.MustReadLibraryName("fuchsia.baz.qux"),
+			Range: state.Range{
+				Start: state.Position{Line: 3, Character: 6},
+				End:   state.Position{Line: 3, Character: 21},
+			},
+		},
+	}
+	if len(imports) != len(expImports) {
+		t.Errorf("expected %d imports, found %d", len(expImports), len(imports))
+	}
+	for i, expImport := range expImports {
+		if imports[i] != expImport {
+			t.Errorf("unexpected import `%v`, expected `%v`", imports[i], expImport)
+		}
+	}
+}
+
+func TestSymbolAtPos(t *testing.T) {
+	fs := state.NewFileSystem()
+	fs.NewFile("test.fidl", `
+library example;
+using test.import;
+
+struct Foo {
+    uint8 bar_baz;
+};
+`)
+
+	cases := []struct {
+		pos state.Position
+		sym state.Symbol
+	}{
+		{
+			pos: state.Position{Line: 1, Character: 0},
+			sym: state.Symbol{
+				Name: "library",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 1, Character: 0},
+						End:   state.Position{Line: 1, Character: 7},
+					},
+				},
+			},
+		},
+		{
+			pos: state.Position{Line: 1, Character: 15},
+			sym: state.Symbol{
+				Name: "example",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 1, Character: 8},
+						End:   state.Position{Line: 1, Character: 15},
+					},
+				},
+			},
+		},
+		{
+			pos: state.Position{Line: 2, Character: 11},
+			sym: state.Symbol{
+				Name: "test.import",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 2, Character: 6},
+						End:   state.Position{Line: 2, Character: 17},
+					},
+				},
+			},
+		},
+		{
+			pos: state.Position{Line: 4, Character: 10},
+			sym: state.Symbol{
+				Name: "Foo",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 4, Character: 7},
+						End:   state.Position{Line: 4, Character: 10},
+					},
+				},
+			},
+		},
+		{
+			pos: state.Position{Line: 5, Character: 13},
+			sym: state.Symbol{
+				Name: "bar_baz",
+				Location: state.Location{
+					FileID: state.FileID("test.fidl"),
+					Range: state.Range{
+						Start: state.Position{Line: 5, Character: 10},
+						End:   state.Position{Line: 5, Character: 17},
+					},
+				},
+			},
+		},
+	}
+
+	for _, ex := range cases {
+		sym, err := fs.SymbolAtPos(
+			state.FileID("test.fidl"),
+			ex.pos,
+		)
+		if err != nil {
+			t.Fatalf("error finding symbol: %s", err)
+		}
+		if sym.Name != ex.sym.Name {
+			t.Fatalf("did not find correct symbol; expected %s, found %s", ex.sym.Name, sym.Name)
+		}
+		if sym.Location.FileID != ex.sym.Location.FileID {
+			t.Fatalf(
+				"incorrect file; expected %s, found %s",
+				ex.sym.Location.FileID,
+				sym.Location.FileID,
+			)
+		}
+		if sym.Location.Range != ex.sym.Location.Range {
+			t.Fatalf(
+				"incorrect range; expected %#v, found %#v",
+				ex.sym.Location.Range,
+				sym.Location.Range,
+			)
+		}
+	}
+}
+
+func TestNoSymbolAtPos(t *testing.T) {
+	fs := state.NewFileSystem()
+	fs.NewFile("test.fidl", `
+//							 ^ should not be a symbol
+library example;
+//			   ^ should not be a symbol
+
+struct Foo {
+//		   ^ should not be a symbol
+    uint8 foo;
+//^ should not be a symbol
+};
+
+.prepended.dot
+^ should not be a symbol ('prepended.dot' is a valid symbol)
+trailing.dot.
+			^ should not be a symbol  ('trailing.dot' is a valid symbol)
+0starts_with_a_number
+	^ should not be a symbol
+ends_with_a_semicolon_
+	^ should not be a symbol
+`)
+
+	cases := []state.Position{
+		{Line: 0, Character: 0},
+		{Line: 2, Character: 16},
+		{Line: 5, Character: 11},
+		{Line: 7, Character: 2},
+		{Line: 11, Character: 0},
+		{Line: 13, Character: 13},
+		{Line: 15, Character: 3},
+		{Line: 17, Character: 3},
+	}
+
+	for _, ex := range cases {
+		sym, err := fs.SymbolAtPos(
+			state.FileID("test.fidl"),
+			ex,
+		)
+		if err == nil {
+			t.Errorf("expected not to find symbol at position %v, but found %s", ex, sym.Name)
+		}
+	}
+}
diff --git a/fidl-lsp/third_party/common/copy_test.go b/fidl-lsp/third_party/common/copy_test.go
new file mode 100644
index 0000000..199e066
--- /dev/null
+++ b/fidl-lsp/third_party/common/copy_test.go
@@ -0,0 +1,32 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package common
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+)
+
+func TestCodegenImplDotGo(t *testing.T) {
+	path, err := os.Getwd()
+	if err != nil {
+		t.Fatalf("error getting cwd: %s", err)
+	}
+	identifiersDotGo, err := ioutil.ReadFile(fmt.Sprintf("%s/identifiers.go", path))
+	if err != nil {
+		t.Fatalf("error reading identifiers.go in fidl-lsp: %s", err)
+	}
+	fidlgenCommonDir := fmt.Sprintf("%s/garnet/go/src/fidl/compiler/backend/common", os.Getenv("FUCHSIA_DIR"))
+	actualIdentifiersDotGo, err := ioutil.ReadFile(fmt.Sprintf("%s/identifiers.go", fidlgenCommonDir))
+	if err != nil {
+		t.Fatalf("error reading identifiers.go in fuchsia.git: %s", err)
+	}
+	if bytes.Compare(identifiersDotGo, actualIdentifiersDotGo) != 0 {
+		t.Fatalf("common/identifers.go is out of date from fuchsia.git, please run copy_fidlgen_common.py")
+	}
+}
diff --git a/fidl-lsp/third_party/common/identifiers.go b/fidl-lsp/third_party/common/identifiers.go
new file mode 100644
index 0000000..85d3b9d
--- /dev/null
+++ b/fidl-lsp/third_party/common/identifiers.go
@@ -0,0 +1,109 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package common
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+)
+
+// LibraryName represents a FIDL library name, such as `fuchsia.mem` or
+// `fuchsia.ui.scenic`.
+type LibraryName struct {
+	fqn string
+}
+
+// Parts returns the library name in parts, e.g. `fuchsia`, `mem` or
+// `fuchsia`, `ui`, `scenic`.
+func (name LibraryName) Parts() []string {
+	return strings.Split(name.fqn, ".")
+}
+
+// FullyQualifiedName returns the fully qualified name, e.g. `fuchsia.mem` or
+// `fuchsia.ui.scenic`.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func (name LibraryName) FullyQualifiedName() string {
+	return name.fqn
+}
+
+// Name represents a FIDL declaration name, consisting of a FIDL library, and
+// a FIDL declration such as `fuchsia.mem` and `Buffer`.
+type Name struct {
+	libraryName LibraryName
+	declName    string
+}
+
+// LibraryName returns the library name, e.g. `fuchsia.mem`.
+func (name Name) LibraryName() LibraryName {
+	return name.libraryName
+}
+
+// DeclarationName returns the declaration name, e.g. `Buffer`.
+func (name Name) DeclarationName() string {
+	return name.declName
+}
+
+// FullyQualifiedName returns the fully qualified name, e.g. `fuchsia.mem/Buffer`.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func (name Name) FullyQualifiedName() string {
+	return fmt.Sprintf("%s/%s", name.libraryName.fqn, name.declName)
+}
+
+var checkLibraryName = regexp.MustCompile("^[a-z][a-z0-9]*(\\.[a-z][a-z0-9]*)*$")
+
+// ReadLibraryName reads a library name from a fully qualified name.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func ReadLibraryName(fullyQualifiedName string) (LibraryName, error) {
+	if !checkLibraryName.MatchString(fullyQualifiedName) {
+		return LibraryName{}, fmt.Errorf("invalid library name: %s", fullyQualifiedName)
+	}
+	return LibraryName{fullyQualifiedName}, nil
+}
+
+// MustReadLibraryName reads a library name from a fully qualified name, and
+// panics in case of error.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func MustReadLibraryName(fullyQualifiedName string) LibraryName {
+	name, err := ReadLibraryName(fullyQualifiedName)
+	if err != nil {
+		panic(err.Error())
+	}
+	return name
+}
+
+// ReadName reads a name from a fully qualified name.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func ReadName(fullyQualifiedName string) (Name, error) {
+	parts := strings.Split(fullyQualifiedName, "/")
+	if len(parts) != 2 {
+		return Name{}, fmt.Errorf("expected a fully qualified name library.name/DeclarationName, found %s", fullyQualifiedName)
+	}
+	libraryName, err := ReadLibraryName(parts[0])
+	if err != nil {
+		return Name{}, err
+	}
+	return Name{
+		libraryName: libraryName,
+		declName:    parts[1],
+	}, nil
+}
+
+// MustReadName reads a name from a fully qualified name, and panics in case of
+// error.
+//
+// See https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/ftp/ftp-043#fully_qualified_names
+func MustReadName(fullyQualifiedName string) Name {
+	name, err := ReadName(fullyQualifiedName)
+	if err != nil {
+		panic(err.Error())
+	}
+	return name
+}
diff --git a/fidldev/OWNERS b/fidldev/OWNERS
new file mode 100644
index 0000000..aefdd75
--- /dev/null
+++ b/fidldev/OWNERS
@@ -0,0 +1,11 @@
+apang@google.com
+bprosnitz@google.com
+fcz@google.com
+godtamit@google.com
+ianloic@google.com
+mkember@google.com
+pascallouis@google.com
+peterjohnston@google.com
+yifeit@google.com
+
+# COMPONENT: FIDL
diff --git a/fidldev/README.md b/fidldev/README.md
new file mode 100644
index 0000000..03e6bcf
--- /dev/null
+++ b/fidldev/README.md
@@ -0,0 +1,25 @@
+# fidldev
+
+`fidldev` is a FIDL development workflow tool. Its goal is to automate
+repetitive processes while working on FIDL code, like running tests based on
+changed files, and regenerating golden files. It is also meant to be the
+source of truth for FIDL code locations and tests/regen commands.
+
+## Running fidldev:
+
+    $FIDLMISC_DIR/fidldev/fidldev.py --help
+
+This can be aliased for convenienced:
+
+    alias fidldev=$FIDLMISC_DIR/fidldev/fidldev.py
+
+Note that `fidldev` should be run from a valid repo (e.g. fuchsia.git, topaz,
+or third_party/go) in order to correctly detect changes, similar to
+`fx format-code`.
+
+## Testing fidldev:
+
+    python3 $FIDLMISC_DIR/fidldev/fidldev_test.py -b
+
+The `-b` flag will separate stdout output when printing test results. It can
+be removed when using debugging print statements in the test itself.
diff --git a/fidldev/env.py b/fidldev/env.py
new file mode 100644
index 0000000..f34ecfc
--- /dev/null
+++ b/fidldev/env.py
@@ -0,0 +1,24 @@
+"""
+Constants that come from the environment. These are in a separate module so
+that they can be mocked by tests if necessary.
+"""
+import os
+from pathlib import Path
+import sys
+
+FUCHSIA_DIR = Path(os.environ["FUCHSIA_DIR"])
+assert FUCHSIA_DIR.exists()
+
+with open(FUCHSIA_DIR / ".fx-build-dir") as f:
+    BUILD_DIR = f.read().strip()
+
+if sys.platform.startswith('linux'):
+    PLATFORM = 'linux'
+elif sys.platform == 'darwin':
+    PLATFORM = 'mac'
+else:
+    print("Unsupported platform: " + sys.platform)
+    sys.exit(1)
+
+with open(FUCHSIA_DIR / (BUILD_DIR + '.zircon') / 'args.gn') as f:
+    MODE = 'asan' if 'asan' in f.read() else 'clang'
diff --git a/fidldev/fidldev.py b/fidldev/fidldev.py
new file mode 100755
index 0000000..3bd6c06
--- /dev/null
+++ b/fidldev/fidldev.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+
+import argparse
+import collections
+import enum
+import subprocess
+import sys
+
+import regen
+import test_
+import util
+
+EXAMPLES = """
+Examples:
+
+Regen goldens and checked in bindings based on changed files in the current
+repo
+
+    fidldev regen
+
+Explicitly specify regen scripts:
+
+    fidldev regen fidlc fidlgen_dart
+    fidldev regen all
+
+Check which regen commands should be run:
+
+    fidldev regen --dry-run --no-build
+
+Run tests based on changed files in the current repo:
+
+    fidldev test
+
+Explicitly specify tests:
+
+    fidldev test fidlc hlcpp llcpp c
+
+Interactively filter test targets:
+
+    fidldev test --interactive
+
+Check which tests should be run:
+
+    fidldev test --dry-run --no-build --no-regen
+
+Pass flags to invocations of fx test:
+
+    fidldev test --fx-test-args "-v -o --dry"
+
+"""
+
+
+def test(args):
+    success = True
+    if args.targets:
+        if not args.no_regen:
+            util.print_warning(
+                'explicit test targets provided, skipping regen...')
+        success = test_.test_explicit(
+            args.targets, not args.no_build, args.dry_run, args.interactive,
+            args.fx_test_args)
+    else:
+        changed_files = util.get_changed_files()
+        if not args.no_regen:
+            regen.regen_changed(changed_files, not args.no_build, args.dry_run)
+            changed_files = util.get_changed_files()
+        success = test_.test_changed(
+            changed_files, not args.no_build, args.dry_run, args.interactive,
+            args.fx_test_args)
+        if args.dry_run:
+            print_dryrun_warning()
+    if not success:
+        sys.exit(1)
+
+
+def regen_cmd(args):
+    if args.targets:
+        regen.regen_explicit(args.targets, not args.no_build, args.dry_run)
+    else:
+        changed_files = util.get_changed_files()
+        regen.regen_changed(changed_files, not args.no_build, args.dry_run)
+        if args.dry_run:
+            print_dryrun_warning()
+
+
+def print_dryrun_warning():
+    print(
+        'NOTE: dry run is conservative and assumes that regen will '
+        'always change files. If goldens do not change during an actual '
+        'run, fewer tests/regen scripts may be run.')
+
+
+parser = argparse.ArgumentParser(
+    description="FIDL development workflow tool",
+    formatter_class=argparse.RawDescriptionHelpFormatter,
+    epilog=EXAMPLES)
+subparsers = parser.add_subparsers()
+
+test_parser = subparsers.add_parser("test", help="Test your FIDL changes")
+test_parser.set_defaults(func=test)
+test_targets = [name for (name, _) in test_.TEST_GROUPS] + ['all']
+test_parser.add_argument(
+    'targets',
+    metavar='target',
+    nargs='*',
+    help=
+    "Manually specify targets to regen, where a target is one of {}. Omit positional arguments to test based on changed files"
+    .format(test_targets))
+test_parser.add_argument(
+    "--dry-run",
+    "-n",
+    help="Print out test commands without running",
+    action="store_true",
+)
+test_parser.add_argument(
+    "--no-build",
+    "-b",
+    help="Don't rebuild targets used for testing",
+    action="store_true",
+)
+test_parser.add_argument(
+    "--no-regen",
+    "-r",
+    help="Don't regen goldens before running tests",
+    action="store_true",
+)
+test_parser.add_argument(
+    "--interactive",
+    "-i",
+    help="Interactively filter tests to be run",
+    action="store_true",
+)
+test_parser.add_argument(
+    "--fx-test-args",
+    "-t",
+    help=
+    "Extra flags and arguments to pass to any invocations of fx test. The flag value is passed verbatim. By default, only '-v' is used.",
+    default='-v',
+)
+
+regen_parser = subparsers.add_parser("regen", help="Run regen commands")
+regen_parser.set_defaults(func=regen_cmd)
+regen_targets = [name for (name, _) in regen.REGEN_TARGETS] + ['all']
+regen_parser.add_argument(
+    'targets',
+    metavar='target',
+    nargs='*',
+    help=
+    "Manually specify targets to regen, where a target is one of {}. Omit positional arguments to regen based on changed files"
+    .format(regen_targets))
+regen_parser.add_argument(
+    "--dry-run",
+    "-n",
+    help="Print out commands without running them",
+    action="store_true",
+)
+regen_parser.add_argument(
+    "--no-build",
+    "-b",
+    help="Don't rebuild targets used for regen",
+    action="store_true",
+)
+
+if __name__ == '__main__':
+    args = parser.parse_args()
+    args.func(args)
diff --git a/fidldev/fidldev_test.py b/fidldev/fidldev_test.py
new file mode 100644
index 0000000..0dd557a
--- /dev/null
+++ b/fidldev/fidldev_test.py
@@ -0,0 +1,414 @@
+import itertools
+import os
+import unittest
+
+from test_util import get_commands, MOCK_FUCHSIA_DIR, MOCK_BUILD_DIR
+import regen
+import util
+
+
+# Many mocks contain 3 return values, because the regen script can call
+# get_changed_files up to 3 times:
+#   1. get the initial set of changed files
+#   2. get changed files after fidlc regen, to check if fidlgen needs regen
+#   3. get changed files after fidlgen regen, to check if go needs regen
+class TestFidlDevRegen(unittest.TestCase):
+
+    def test_basic_regen(self):
+        mocks = {
+            'get_changed_files':
+                itertools.repeat(['zircon/tools/fidl/lib/flat_ast.cc']),
+        }
+        command = ['regen']
+        expected = [
+            util.BUILD_FIDLC,
+            regen.path_to_regen_command(regen.FIDLC_REGEN),
+        ]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_ir_changed(self):
+        mocks = {
+            'get_changed_files':
+                [
+                    ['zircon/tools/fidl/lib/parser.cc'],
+                    [
+                        'zircon/tools/fidl/lib/parser.cc',
+                        'zircon/tools/fidl/goldens/bits.test.json.golden'
+                    ],
+                    [
+                        'zircon/tools/fidl/lib/parser.cc',
+                        'zircon/tools/fidl/goldens/bits.test.json.golden'
+                    ],
+                ]
+        }
+        command = ['regen']
+        expected = [
+            util.BUILD_FIDLC,
+            regen.path_to_regen_command(regen.FIDLC_REGEN), util.BUILD_FIDLGEN,
+            regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+            util.BUILD_FIDLGEN_DART,
+            regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN)
+        ]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_tables_changed(self):
+        mocks = {
+            'get_changed_files':
+                [
+                    ['zircon/tools/fidl/lib/parser.cc'],
+                    [
+                        'zircon/tools/fidl/lib/parser.cc',
+                        'zircon/tools/fidl/goldens/bits.test.tables.c.golden'
+                    ],
+                    [
+                        'zircon/tools/fidl/lib/parser.cc',
+                        'zircon/tools/fidl/goldens/bits.test.tables.c.golden'
+                    ],
+                ]
+        }
+        command = ['regen']
+        expected = [
+            util.BUILD_FIDLC,
+            regen.path_to_regen_command(regen.FIDLC_REGEN),
+        ]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_go_goldens_changed(self):
+        mocks = {
+            'get_changed_files':
+                [
+                    ['zircon/tools/fidl/lib/parser.cc'],
+                    [
+                        'zircon/tools/fidl/lib/parser.cc',
+                        'zircon/tools/fidl/goldens/bits.test.json.golden'
+                    ],
+                    [
+                        'zircon/tools/fidl/lib/parser.cc',
+                        'zircon/tools/fidl/goldens/bits.test.json.golden',
+                        'garnet/go/src/fidl/compiler/backend/goldens/union.test.json.go.golden'
+                    ],
+                ]
+        }
+        command = ['regen']
+        expected = [
+            util.BUILD_FIDLC,
+            regen.path_to_regen_command(regen.FIDLC_REGEN),
+            util.BUILD_FIDLGEN,
+            regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+            util.BUILD_FIDLGEN_DART,
+            regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+            regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+        ]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_rust_goldens_changed(self):
+        mocks = {
+            'get_changed_files':
+                [
+                    ['zircon/tools/fidl/lib/parser.cc'],
+                    [
+                        'zircon/tools/fidl/lib/parser.cc',
+                        'zircon/tools/fidl/goldens/bits.test.json.golden'
+                    ],
+                    [
+                        'zircon/tools/fidl/lib/parser.cc',
+                        'zircon/tools/fidl/goldens/bits.test.json.golden',
+                        'garnet/go/src/fidl/compiler/backend/goldens/union.test.json.rs.golden'
+                    ],
+                ]
+        }
+        command = ['regen']
+        expected = [
+            util.BUILD_FIDLC,
+            regen.path_to_regen_command(regen.FIDLC_REGEN),
+            util.BUILD_FIDLGEN,
+            regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+            util.BUILD_FIDLGEN_DART,
+            regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+        ]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_fidlgen_go_changed(self):
+        mocks = {
+            'get_changed_files':
+                itertools.repeat(['tools/fidl/fidlgen_go/ir/ir.go'])
+        }
+        command = ['regen']
+        expected = [
+            util.BUILD_FIDLGEN,
+            regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+            regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+        ]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_fidlgen_changed(self):
+        mocks = {
+            'get_changed_files':
+                itertools.repeat(
+                    ['tools/fidl/fidlgen_syzkaller/templates/struct.tmpl.go'])
+        }
+        command = ['regen']
+        expected = [
+            util.BUILD_FIDLGEN,
+            regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+        ]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_fidlgen_dart_changed(self):
+        mocks = {
+            'get_changed_files':
+                itertools.repeat(['topaz/bin/fidlgen_dart/fidlgen_dart.go'])
+        }
+        command = ['regen']
+        expected = [
+            util.BUILD_FIDLGEN_DART,
+            regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+        ]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_regen_all(self):
+        command = ['regen', 'all']
+        expected = [
+            util.BUILD_FIDLC,
+            regen.path_to_regen_command(regen.FIDLC_REGEN),
+            util.BUILD_FIDLGEN,
+            regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+            util.BUILD_FIDLGEN_DART,
+            regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+            regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+        ]
+        self.assertListEqual(get_commands({}, command), expected)
+
+    def test_regen_no_build(self):
+        command = ['regen', 'all', '--no-build']
+        expected = [
+            regen.path_to_regen_command(regen.FIDLC_REGEN),
+            regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+            regen.path_to_regen_command(regen.FIDLGEN_DART_REGEN),
+            regen.path_to_regen_command(regen.GO_BINDINGS_REGEN),
+        ]
+        self.assertListEqual(get_commands({}, command), expected)
+
+    def test_regen_fidlgen(self):
+        command = ['regen', 'fidlgen']
+        expected = [
+            util.BUILD_FIDLGEN,
+            regen.path_to_regen_command(regen.FIDLGEN_REGEN),
+        ]
+        self.assertListEqual(get_commands({}, command), expected)
+
+
+class TestFidlDevTest(unittest.TestCase):
+
+    def test_no_changes(self):
+        mocks = {'get_changed_files': [[]]}
+        command = ['test', '--no-regen']
+        self.assertListEqual(get_commands(mocks, command), [])
+
+    def test_fidlc_changed(self):
+        mocks = {'get_changed_files': [['zircon/tools/fidl/lib/parser.cc']]}
+        command = ['test', '--no-regen']
+        expected = [util.BUILD_FIDLC_TESTS, util.TEST_FIDLC]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_ir_changed_zircon(self):
+        mocks = {
+            'get_changed_files':
+                [['zircon/tools/fidl/goldens/bits.test.json.golden']]
+        }
+        command = ['test', '--no-regen']
+        expected = [util.BUILD_FIDLC_TESTS, util.TEST_FIDLC]
+        self.assertListEqual(get_commands(mocks, command), expected)
+
+    def test_ir_changed(self):
+        mocks = {
+            'get_changed_files':
+                [
+                    [
+                        'garnet/go/src/fidl/compiler/backend/goldens/struct.test.json',
+                    ]
+                ]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = set(util.FIDLGEN_TEST_TARGETS)
+        self.assertEqual(len(actual), 2)
+        self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+        self.assertTestsRun(actual[1], expected)
+
+    def test_fidlgen_util_changed(self):
+        mocks = {
+            'get_changed_files':
+                [[
+                    'garnet/go/src/fidl/compiler/backend/types/types.go',
+                ]]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = set(util.FIDLGEN_TEST_TARGETS)
+        self.assertEqual(len(actual), 2)
+        self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+        self.assertTestsRun(actual[1], expected)
+
+    def test_fidlgen_backend_changed(self):
+        mocks = {
+            'get_changed_files':
+                [[
+                    'tools/fidl/fidlgen_rust/templates/enum.tmpl.go',
+                ]]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = set(util.FIDLGEN_TEST_TARGETS)
+        self.assertEqual(len(actual), 2)
+        self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+        self.assertTestsRun(actual[1], expected)
+
+    def test_fidlgen_golden_changed(self):
+        mocks = {
+            'get_changed_files':
+                [
+                    [
+                        'garnet/go/src/fidl/compiler/backend/goldens/union.test.json.cc.golden',
+                    ]
+                ]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = set(util.FIDLGEN_TEST_TARGETS) | {util.HLCPP_TEST_TARGET}
+        self.assertEqual(len(actual), 2)
+        self.assertEqual(actual[0], util.BUILD_FIDLGEN)
+        self.assertTestsRun(actual[1], expected)
+
+    def test_fidlgen_dart_changed(self):
+        mocks = {
+            'get_changed_files': [[
+                'topaz/bin/fidlgen_dart/backend/ir/ir.go',
+            ]]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = {util.FIDLGEN_DART_TEST_TARGET}
+        self.assertEqual(len(actual), 2)
+        self.assertEqual(actual[0], util.BUILD_FIDLGEN_DART)
+        self.assertTestsRun(actual[1], expected)
+
+    def test_fidlgen_dart_golden_changed(self):
+        mocks = {
+            'get_changed_files':
+                [
+                    [
+                        'topaz/bin/fidlgen_dart/goldens/handles.test.json_async.dart.golden',
+                    ]
+                ]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = {
+            util.DART_TEST_TARGET,
+            util.FIDLGEN_DART_TEST_TARGET,
+        }
+        self.assertEqual(len(actual), 2)
+        self.assertEqual(actual[0], util.BUILD_FIDLGEN_DART)
+        self.assertTestsRun(actual[1], expected)
+
+    def test_c_runtime_changed(self):
+        mocks = {
+            'get_changed_files': [[
+                'zircon/system/ulib/fidl/txn_header.c',
+            ]]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = {
+            util.HLCPP_TEST_TARGET,
+            util.LLCPP_TEST_TARGET,
+            util.C_TEST_TARGET,
+        }
+        self.assertEqual(len(actual), 1)
+        self.assertTestsRun(actual[0], expected)
+
+    def test_coding_tables_changed(self):
+        mocks = {
+            'get_changed_files':
+                [[
+                    'zircon/tools/fidl/goldens/union.test.tables.c.golden',
+                ]]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = {
+            util.HLCPP_TEST_TARGET,
+            util.LLCPP_TEST_TARGET,
+            util.C_TEST_TARGET,
+        }
+        self.assertEqual(actual[1], util.TEST_FIDLC)
+        self.assertTestsRun(actual[2], expected)
+
+    def test_go_runtime_changed(self):
+        mocks = {
+            'get_changed_files':
+                [[
+                    'third_party/go/src/syscall/zx/fidl/encoding_new.go',
+                ]]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = {
+            util.GO_CONFORMANCE_TEST_TARGET,
+            util.GO_TEST_TARGET,
+        }
+        self.assertEqual(len(actual), 1)
+        self.assertTestsRun(actual[0], expected)
+
+    def test_dart_runtime_changed(self):
+        mocks = {
+            'get_changed_files':
+                [
+                    [
+                        'topaz/public/dart/fidl/lib/src/types.dart',
+                        'topaz/public/dart/fidl/lib/src/message.dart',
+                    ]
+                ]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = {util.DART_TEST_TARGET}
+        self.assertEqual(len(actual), 1)
+        self.assertTestsRun(actual[0], expected)
+
+    def test_gidl_changed(self):
+        mocks = {
+            'get_changed_files':
+                [
+                    [
+                        'tools/fidl/gidl/main.go ',
+                        'tools/fidl/gidl/rust/benchmarks.go',
+                        'tools/fidl/gidl/rust/conformance.go',
+                    ]
+                ]
+        }
+        command = ['test', '--no-regen']
+        actual = get_commands(mocks, command)
+        expected = {
+            util.GIDL_TEST_TARGET,
+            util.GO_CONFORMANCE_TEST_TARGET,
+            util.HLCPP_CONFORMANCE_TEST_TARGET,
+            util.HLCPP_HOST_CONFORMANCE_TEST_TARGET,
+            util.LLCPP_CONFORMANCE_TEST_TARGET,
+            util.RUST_CONFORMANCE_TEST_TARGET,
+            util.DART_TEST_TARGET,
+        }
+        self.assertEqual(len(actual), 1)
+        self.assertTestsRun(actual[0], expected)
+
+    def assertTestsRun(self, raw_command, expected):
+        self.assertEqual(raw_command[0], 'fx')
+        self.assertEqual(raw_command[1], 'test')
+        self.assertEqual(raw_command[2], '-v')
+        tests = set(raw_command[3:])
+        self.assertSetEqual(tests, expected)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/fidldev/regen.py b/fidldev/regen.py
new file mode 100644
index 0000000..55dc63d
--- /dev/null
+++ b/fidldev/regen.py
@@ -0,0 +1,106 @@
+import os
+
+import util
+
+FIDLC_REGEN = 'zircon/tools/fidl/testdata/regen.sh'
+FIDLGEN_REGEN = 'garnet/go/src/fidl/compiler/backend/typestest/regen.sh'
+FIDLGEN_DART_REGEN = 'topaz/bin/fidlgen_dart/regen.sh'
+GO_BINDINGS_REGEN = 'third_party/go/regen-fidl'
+
+
+def regen_fidlc_goldens(build_first, dry_run):
+    if build_first:
+        util.run(util.BUILD_FIDLC, dry_run, exit_on_failure=True)
+    util.run(path_to_regen_command(FIDLC_REGEN), dry_run, exit_on_failure=True)
+
+
+def regen_fidlgen_goldens(build_first, dry_run):
+    if build_first:
+        util.run(util.BUILD_FIDLGEN, dry_run, exit_on_failure=True)
+    util.run(
+        path_to_regen_command(FIDLGEN_REGEN), dry_run, exit_on_failure=True)
+
+
+def regen_fidlgendart_goldens(build_first, dry_run):
+    if build_first:
+        util.run(util.BUILD_FIDLGEN_DART, dry_run, exit_on_failure=True)
+    util.run(
+        path_to_regen_command(FIDLGEN_DART_REGEN),
+        dry_run,
+        exit_on_failure=True)
+
+
+def regen_go_bindings(build_first, dry_run, exit_on_failure=True):
+    util.run(
+        path_to_regen_command(GO_BINDINGS_REGEN), dry_run, exit_on_failure=True)
+
+
+def path_to_regen_command(path):
+    return ['fx', 'exec', os.path.join(util.FUCHSIA_DIR, path)]
+
+
+def is_ir_changed():
+    for path in util.get_changed_files():
+        if path.startswith(
+                util.FIDLC_DIR) and path.endswith('.test.json.golden'):
+            return True
+    return False
+
+
+def is_go_bindings_changed():
+    for path in util.get_changed_files():
+        if path.startswith(
+                util.FIDLGEN_DIR) and path.endswith('.test.json.go.golden'):
+            return True
+    return False
+
+
+REGEN_TARGETS = [
+    ('fidlc', regen_fidlc_goldens),
+    ('fidlgen', regen_fidlgen_goldens),
+    ('fidlgen_dart', regen_fidlgendart_goldens),
+    ('go', regen_go_bindings),
+]
+
+
+def regen_explicit(targets, build_first, dry_run):
+    for target, func in REGEN_TARGETS:
+        if target in targets or 'all' in targets:
+            func(build_first, dry_run)
+
+
+def regen_changed(changed_files, build_first, dry_run):
+    regen_fidlc = False
+    regen_fidlgen = False
+    regen_fidlgendart = False
+    regen_go = False
+    for file_ in changed_files:
+        if file_.startswith(util.FIDLC_DIR):
+            regen_fidlc = True
+        if file_.startswith(util.FIDLGEN_DIR) or is_in_fidlgen_backend(file_):
+            regen_fidlgen = True
+        if file_.startswith(util.FIDLGEN_DART_DIR):
+            regen_fidlgendart = True
+        if file_.startswith(util.FIDLGEN_GO_DIR):
+            regen_go = True
+
+    if regen_fidlc:
+        regen_fidlc_goldens(build_first, dry_run)
+        if dry_run or is_ir_changed():
+            regen_fidlgen = True
+            regen_fidlgendart = True
+
+    if regen_fidlgen:
+        regen_fidlgen_goldens(build_first, dry_run)
+        if dry_run or is_go_bindings_changed():
+            regen_go = True
+
+    if regen_fidlgendart:
+        regen_fidlgendart_goldens(build_first, dry_run)
+
+    if regen_go:
+        regen_go_bindings(build_first, dry_run)
+
+
+def is_in_fidlgen_backend(path):
+    return any(path.startswith(b) for b in util.FIDLGEN_BACKEND_DIRS)
diff --git a/fidldev/test_.py b/fidldev/test_.py
new file mode 100644
index 0000000..ba438b7
--- /dev/null
+++ b/fidldev/test_.py
@@ -0,0 +1,201 @@
+import functools
+import pprint
+import util
+import regen
+
+
+def startswith(prefix):
+    return lambda s: s.startswith(prefix)
+
+
+def endswith(suffix):
+    return lambda s: s.endswith(suffix)
+
+
+# List of test groups. Each test group is of the following structure:
+#   (NAME, (PREDICATES, TARGETS, BUILD COMMAND))
+# where:
+#   - NAME is a name for the group of tests. This name is used to explicitly
+#     invoke this test group on the command line (e.g. fidldev test foo would call
+#     fx test on the TARGETS for group foo)
+#   - PREDICATES is a list of predicates P such that P(file) returns true if the
+#     test group should run when |file| is changed
+#   - TARGETS is a list of test names as supported by `fx test`, e.g.
+#     fully-formed Fuchsia Package URLs, package names, or directories
+#   - BUILD COMMAND is any command that should be run prior to running the test
+#     group. It can be None if no build step is required, and is skipped if the
+#     --no-build flag is passed in. It currently needs to be a List - run_tests
+#     needs to be updated to support strings
+TEST_GROUPS = [
+    (
+        'fidlc', (
+            [startswith('zircon/tools/fidl')], [util.TEST_FIDLC],
+            util.BUILD_FIDLC_TESTS)),
+
+    # it's possible to be more selective on which changes map to which tests,
+    # but since fidlgen tests are fast to build and run, just do a blanket
+    # check.
+    (
+        'fidlgen', (
+            [startswith(util.FIDLGEN_DIR)] +
+            [startswith(p) for p in util.FIDLGEN_BACKEND_DIRS],
+            util.FIDLGEN_TEST_TARGETS, util.BUILD_FIDLGEN)),
+    (
+        'fidlgen_dart', (
+            [startswith(util.FIDLGEN_DART_DIR)],
+            [util.FIDLGEN_DART_TEST_TARGET], util.BUILD_FIDLGEN_DART)),
+    (
+        'hlcpp', (
+            [
+                endswith('test.json.cc.golden'),
+                endswith('test.json.h.golden'),
+                endswith('test.tables.c.golden'),
+                startswith(util.HLCPP_RUNTIME),
+                startswith(util.C_RUNTIME),
+            ], [util.HLCPP_TEST_TARGET], None)),
+    (
+        'llcpp', (
+            [
+                endswith('test.json.llcpp.cc.golden'),
+                endswith('test.json.llcpp.h.golden'),
+                endswith('test.tables.c.golden'),
+                startswith(util.LLCPP_RUNTIME),
+                startswith(util.C_RUNTIME)
+            ], [util.LLCPP_TEST_TARGET], None)),
+    (
+        'c',
+        (
+            # normally, changes to the generated bindings are detected by looking at the
+            # goldens. Since we can't do this for C, we look at the coding table goldens
+            # and the c_generator instead.
+            [
+                endswith('test.tables.c.golden'),
+                startswith('zircon/tools/fidl/include/fidl/c_generator.h'),
+                startswith('zircon/tools/fidl/lib/c_generator.cc'),
+                startswith(util.C_RUNTIME),
+            ],
+            # NOTE: fidl-test should also run, but this script only supports component
+            # tests
+            [util.C_TEST_TARGET],
+            None)),
+    (
+        'go', (
+            [endswith('test.json.go.golden'),
+             startswith(util.GO_RUNTIME)],
+            [util.GO_TEST_TARGET, util.GO_CONFORMANCE_TEST_TARGET], None)),
+    (
+        'rust', (
+            [endswith('test.json.rs.golden'),
+             startswith(util.RUST_RUNTIME)], [util.RUST_TEST_TARGET], None)),
+    (
+        'dart', (
+            [
+                endswith('test.json_async.dart.golden'),
+                endswith('test.json_test.dart.golden'),
+                startswith(util.DART_RUNTIME)
+            ], [util.DART_TEST_TARGET], None)),
+    (
+        'gidl',
+        (
+            [startswith('tools/fidl/gidl')],
+            [
+                util.GIDL_TEST_TARGET,
+                util.GO_CONFORMANCE_TEST_TARGET,
+                util.HLCPP_CONFORMANCE_TEST_TARGET,
+                util.HLCPP_HOST_CONFORMANCE_TEST_TARGET,
+                util.LLCPP_CONFORMANCE_TEST_TARGET,
+                util.RUST_CONFORMANCE_TEST_TARGET,
+                # dart conformance is bundled into the rest of the tests
+                util.DART_TEST_TARGET
+            ],
+            None)),
+]
+
+
+def test_explicit(targets, build_first, dry_run, interactive, fx_test_args):
+    """ Test an explicit set of test groups """
+    tests = []
+    for name, test in TEST_GROUPS:
+        if name in targets or 'all' in targets:
+            tests.append(test)
+    return run_tests(tests, build_first, dry_run, interactive, fx_test_args)
+
+
+def test_changed(
+        changed_files, build_first, dry_run, interactive, fx_test_args):
+    """ Test relevant test groups given a set of changed files """
+    tests = []
+    for _, test in TEST_GROUPS:
+        (predicates, _, _) = test
+        for file_ in changed_files:
+            if any(p(file_) for p in predicates):
+                tests.append(test)
+    return run_tests(tests, build_first, dry_run, interactive, fx_test_args)
+
+
+def run_tests(tests, build_first, dry_run, interactive, fx_test_args):
+    already_built = set()
+    test_targets = set()
+    manual_tests = set()
+    for name, targets, build in tests:
+        if build_first and build is not None and tuple(
+                build) not in already_built:
+            already_built.add(tuple(build))
+            util.run(build, dry_run, exit_on_failure=True)
+
+        for target in targets:
+            if is_manual_test(target):
+                manual_tests.add(target)
+            else:
+                test_targets.add(target)
+
+    manual_tests = list(manual_tests)
+    test_targets = list(test_targets)
+    if interactive:
+        print('all tests: ')
+        pprint.pprint(manual_tests + test_targets)
+        manual_tests = interactive_filter(manual_tests)
+        test_targets = interactive_filter(test_targets)
+
+    success = True
+    for cmd in manual_tests:
+        success = success and util.run(cmd, dry_run)
+        # print test line that can be copied into a commit message
+        # the absolute FUCHSIA_DIR paths are stripped for readability and
+        # because they are user specific
+        print('Test: ' + cmd.replace(str(util.FUCHSIA_DIR) + '/', ''))
+
+    if test_targets:
+        cmd = ['fx', 'test'] + fx_test_args.split()
+        if not build_first:
+            cmd.append('--no-build')
+        # group all tests into a single `fx test` invocation so that the summary
+        # prints all results
+        cmd.extend(test_targets)
+        success = success and util.run(cmd, dry_run)
+        print('Test: ' + ' '.join(cmd))
+
+    return success
+
+
+def interactive_filter(test_targets):
+    if not test_targets:
+        return []
+    filtered = []
+    for test in test_targets:
+        if input('run {}? (Y/n) '.format(test)) == 'n':
+            continue
+        filtered.append(test)
+    return filtered
+
+
+def is_manual_test(test):
+    """
+    Return whether this is meant to be called with fx test or used as a
+    standalone test command.
+    """
+    # currently fidlc is the only test that doesn't use fx test, since it
+    # uses some fidlc/fidl-compiler-test binary that is not built with the
+    # usual build commands (like fx build zircon/tools, fx ninja -C out/default
+    # host_x64/fidlc)
+    return test == util.TEST_FIDLC
diff --git a/fidldev/test_util.py b/fidldev/test_util.py
new file mode 100644
index 0000000..c70ca7c
--- /dev/null
+++ b/fidldev/test_util.py
@@ -0,0 +1,62 @@
+import argparse
+import contextlib
+import pprint
+
+import env
+
+# mock out these environment consts before importing anything else
+MOCK_FUCHSIA_DIR = 'fuchsia_dir'
+MOCK_BUILD_DIR = 'out/default'
+env.FUCHSIA_DIR = MOCK_FUCHSIA_DIR
+env.BUILD_DIR = MOCK_BUILD_DIR
+env.PLATFORM = 'linux'
+env.MODE = 'clang'
+
+import fidldev
+import util
+
+
+@contextlib.contextmanager
+def mocked_func(func_name, mocked_func):
+    """ Patch util.[func_name] with mocked_func within the specified context. """
+    original = getattr(util, func_name)
+    try:
+        setattr(util, func_name, mocked_func)
+        yield
+    finally:
+        setattr(util, func_name, original)
+
+
+def create_fixed_func(return_values):
+    """ Returns a function that successively returns each of the provided |return_values|. """
+    return_values = iter(return_values)
+
+    def mocked(*args, **kwargs):
+        return next(return_values)
+
+    return mocked
+
+
+def get_commands(mocks, test_cmd):
+    """ Run |test_cmd| with the provided |mocks|, and return the commands that fidldev would have run. """
+    mocked_funcs = [
+        mocked_func(name, create_fixed_func(values))
+        for name, values in mocks.items()
+    ]
+
+    commands = []
+
+    # The arguments and return value of this function need to be kept up to date with util.run
+    def mocked_run(command, dry_run, exit_on_failure=False):
+        commands.append(command)
+        return True
+
+    mocked_funcs.append(mocked_func('run', mocked_run))
+
+    with contextlib.ExitStack() as stack:
+        for func in mocked_funcs:
+            stack.enter_context(func)
+        args = fidldev.parser.parse_args(test_cmd)
+        args.func(args)
+
+    return commands
diff --git a/fidldev/util.py b/fidldev/util.py
new file mode 100644
index 0000000..0482dd0
--- /dev/null
+++ b/fidldev/util.py
@@ -0,0 +1,117 @@
+import os
+import subprocess
+
+from env import FUCHSIA_DIR, BUILD_DIR, MODE, PLATFORM
+
+TOPAZ_DIR = os.path.join(FUCHSIA_DIR, 'topaz')
+GO_DIR = os.path.join(FUCHSIA_DIR, 'third_party/go')
+
+FIDLC_DIR = 'zircon/tools/fidl'
+FIDLGEN_DIR = 'garnet/go/src/fidl/compiler'
+FIDLGEN_DART_DIR = 'topaz/bin/fidlgen_dart'
+FIDLGEN_GO_DIR = 'tools/fidl/fidlgen_go'
+FIDLGEN_BACKEND_DIRS = [
+    'garnet/go/src/fidl/compiler/llcpp_backend',
+    FIDLGEN_GO_DIR,
+    'tools/fidl/fidlgen_hlcpp',
+    'tools/fidl/fidlgen_libfuzzer',
+    'tools/fidl/fidlgen_rust',
+    'tools/fidl/fidlgen_syzkaller',
+]
+
+TEST_FIDLC = os.path.join(FUCHSIA_DIR, BUILD_DIR, 'host_x64/fidl-compiler')
+FIDLGEN_TEST_TARGETS = [
+    '//garnet/go/src/fidl',
+    '//tools/fidl/fidlgen_hlcpp',
+    '//tools/fidl/fidlgen_go',
+    '//tools/fidl/fidlgen_libfuzzer',
+    '//tools/fidl/fidlgen_rust',
+    '//tools/fidl/fidlgen_syzkaller',
+]
+FIDLGEN_DART_TEST_TARGET = '//topaz/bin/fidlgen_dart'
+HLCPP_TEST_TARGET = '//sdk/lib/fidl'
+LLCPP_TEST_TARGET = '//src/lib/fidl/llcpp'
+C_TEST_TARGET = '//src/lib/fidl/c'
+GO_TEST_TARGET = 'go_fidl_test'
+RUST_TEST_TARGET = '//src/lib/fidl/rust'
+DART_TEST_TARGET = 'fidl_bindings_test'
+GIDL_TEST_TARGET = '//tools/fidl/gidl'
+
+HLCPP_CONFORMANCE_TEST_TARGET = 'conformance_test'
+HLCPP_HOST_CONFORMANCE_TEST_TARGET = 'fidl_cpp_host_conformance_test'
+LLCPP_CONFORMANCE_TEST_TARGET = 'fidl_llcpp_conformance_test'
+GO_CONFORMANCE_TEST_TARGET = 'fidl_go_conformance'
+RUST_CONFORMANCE_TEST_TARGET = 'fidl_rust_conformance_tests'
+
+HLCPP_RUNTIME = 'sdk/lib/fidl'
+LLCPP_RUNTIME = 'src/lib/fidl/llcpp'
+C_RUNTIME = 'zircon/system/ulib/fidl'
+GO_RUNTIME = 'third_party/go/src/syscall/zx/fidl'
+RUST_RUNTIME = 'src/lib/fidl/rust'
+DART_RUNTIME = 'topaz/public/dart/fidl/lib'
+
+BUILD_FIDLC = ['fx', 'build', 'zircon/tools']
+BUILD_FIDLC_TESTS = ['fx', 'ninja', '-C', BUILD_DIR, 'host_x64/fidl-compiler']
+BUILD_FIDLGEN = ['fx', 'build', 'garnet/go/src/fidl']
+BUILD_FIDLGEN_DART = ['fx', 'ninja', '-C', BUILD_DIR, 'host_x64/fidlgen_dart']
+
+
+def run(command, dry_run, exit_on_failure=False):
+    """
+    Run the given command, returning True if it completed successfuly. If
+    dry_run is true, just prints rather than running. If exit_on_failure is
+    true, exits instead of returning False.
+    """
+    if dry_run:
+        print('would run: {}'.format(command))
+        return True
+    retcode = subprocess.call(command)
+    success = retcode == 0
+    if exit_on_failure and not success:
+        print_err(
+            'Error: command failed with status {}! {}'.format(retcode, command))
+        exit(1)
+    return success
+
+
+def get_changed_files():
+    """
+    Return a List of paths relative to FUCHSIA_DIR of changed files relative to
+    the parent. This uses the same logic as fx format-code.
+    """
+    upstream = "origin/master"
+    local_commit = subprocess.check_output(
+        "git rev-list HEAD ^{} -- 2>/dev/null | tail -1".format(upstream),
+        shell=True).strip().decode()
+    diff_base = subprocess.check_output(
+        ['git', 'rev-parse', local_commit +
+         '^']).strip().decode() if local_commit else "HEAD"
+    files = subprocess.check_output(['git', 'diff', '--name-only',
+                                     diff_base]).strip().decode().split('\n')
+
+    repo = subprocess.check_output(['git', 'rev-parse',
+                                    '--show-toplevel']).strip().decode()
+    # add prefixes so that all and targets can be specified relative to FUCHSIA_DIR
+    if repo.endswith('topaz'):
+        files = [os.path.join('topaz', p) for p in files]
+    elif repo.endswith('third_party/go'):
+        files = [os.path.join('third_party/go', p) for p in files]
+
+    return files
+
+
+RED = '\033[1;31m'
+YELLOW = '\033[1;33m'
+NC = '\033[0m'
+
+
+def print_err(s):
+    print_color(s, RED)
+
+
+def print_warning(s):
+    print_color(s, YELLOW)
+
+
+def print_color(s, color):
+    print('{}{}{}'.format(color, s, NC))
diff --git a/.vscode/launch.json b/vscode-language-fidl/.vscode/launch.json
similarity index 100%
rename from .vscode/launch.json
rename to vscode-language-fidl/.vscode/launch.json