[lsp] Initial commit of FIDL LSP Language Server

This CL includes the basic infrastructure for the FIDL Language Server,
including the server's handling of LSP requests over JSON-RPC, file
synchronization with the client, dependency management and FIDL library
compilation, and two language features: diagnostics (errors/warnings/
lints) and formatting.

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

The core logic of the language server is in the `langserver` package.
This package deals with LSP specific boilerplate, JSON-RPC messages,
etc.
  handler.go:     defines the LangHandler, which takes LSP requests and
                  sends reponses and notifications to the client, and
                  dispatches changes or requests for analyses.
  files.go:       LangHandler methods that dispatch requests to the
                  FileSystem.
  format.go:      LangHandler method that handles formatting request
  diagnostics.go: LangHandler.publishDiagnostics

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.
  fs.go:          defines the FileSystem, which keeps the server's VFS
                  in sync with the client's edits.
  parse.go:       functions that use regex to find `library` and
                  `using` declarations in in-memory FIDL files.

The "backend" of the server, which doesn't know about LSP but knows how
to compile FIDL and analyze the JSON IR, is in the `analysis` package.
  analyzer.go:    definition of Analyzer, the Library type, and the
                  Analyze method, which is called every time a file
                  is changed on the client. Analyze recompiles the
                  relevant FIDL library and reads in the JSON IR.
  compile.go:     used to compile FIDL libraries to get diagnostics.
  deps.go:        contains FindDeps, which is used to generate a fidlc
                  invocation for a given file.
  library.go:     a representation of a deserialized JSON IR. will be
                  used for future language features.
  format.go:      contains FormatFile, which invokes fidl-format on the
                  specified file.
  diagnostics.go: methods to extract diagnostics from fidlc and
                  fidl-lint output.

gen_fidl_project.py is a script to generate a fidl_project.json file,
which 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).

Test: go test ./...
Change-Id: I24536fd5ed1feb0cc16e0d49077959896494a270
Reviewed-on: https://fuchsia-review.googlesource.com/c/fidl-misc/+/392713
Reviewed-by: Pascal Perez <pascallouis@google.com>
Reviewed-by: Benjamin Prosnitz <bprosnitz@google.com>
diff --git a/fidl-lsp/README.md b/fidl-lsp/README.md
new file mode 100644
index 0000000..dc8913d
--- /dev/null
+++ b/fidl-lsp/README.md
@@ -0,0 +1,57 @@
+# 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)
+- [x] [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)
+
+## Lanuguage 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..1d93e56
--- /dev/null
+++ b/fidl-lsp/analysis/analyzer.go
@@ -0,0 +1,204 @@
+// 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"
+	"os"
+
+	"fidl-lsp/state"
+)
+
+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"
+)
+
+// A LibraryName is a FIDL library represented by its name (as spelled in its
+// `library` declaration, not necessarily matching directory or file names).
+type LibraryName 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[LibraryName]bool
+	files map[string]bool
+	json  string
+	ir    *FidlLibrary // nil if not yet imported
+	diags map[state.EditorFile][]Diagnostic
+}
+
+// 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 {
+	// 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[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.EditorFile]string
+
+	// A map from library name to the corresponding tmp file used for their JSON
+	// IR.
+	inputFilesJSON map[LibraryName]string
+}
+
+// NewAnalyzer returns an Analyzer initialized with the set of CompiledLibraries
+// passed in.
+func NewAnalyzer(compiledLibraries CompiledLibraries) *Analyzer {
+	a := &Analyzer{
+		libs:           make(map[LibraryName]*Library),
+		inputFilesFIDL: make(map[state.EditorFile]string),
+		inputFilesJSON: make(map[LibraryName]string),
+	}
+	for libName, lib := range compiledLibraries {
+		a.libs[libName] = &Library{
+			deps:  make(map[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[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.EditorFile) 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[LibraryName(libName)]; !ok {
+			a.libs[LibraryName(libName)] = &Library{
+				deps:  make(map[LibraryName]bool),
+				files: make(map[string]bool),
+				diags: make(map[state.EditorFile][]Diagnostic),
+			}
+		}
+
+		lib := a.libs[LibraryName(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.EditorFileToPath(path); err == nil {
+			if _, ok := lib.files[absPath]; ok {
+				delete(lib.files, absPath)
+			}
+		} else {
+			fmt.Fprint(os.Stderr, "error parsing\n")
+		}
+
+		lib.files[inputFilePath] = true
+		imports := state.ParsePlatformImportsMatch(file)
+		for _, dep := range imports {
+			lib.deps[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.EditorFile) (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) inputFileToEditorFile(inputFilePath string) (state.EditorFile, error) {
+	for editorFile, inputFile := range a.inputFilesFIDL {
+		if inputFile == inputFilePath {
+			return editorFile, 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 LibraryName) string {
+	if path, ok := a.inputFilesJSON[lib]; ok {
+		return path
+	}
+	a.inputFilesJSON[lib] = fmt.Sprintf("/tmp/%d.json", hash(string(lib)))
+	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..fb0ec1a
--- /dev/null
+++ b/fidl-lsp/analysis/compile.go
@@ -0,0 +1,106 @@
+// 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"
+
+	"fidl-lsp/state"
+)
+
+// 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?
+	if _, ok := a.libs[LibraryName(lib.Name)]; !ok {
+		a.libs[LibraryName(lib.Name)] = &Library{
+			json: jsonPath,
+		}
+	}
+	a.libs[LibraryName(lib.Name)].ir = &lib
+
+	// TODO: construct symbol map from JSON IR for the file's library
+	return nil
+}
+
+type compileResult struct {
+	lib   LibraryName
+	diags map[state.EditorFile][]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.EditorFile) (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`", path)
+	}
+	jsonPath = a.pathToJSON(LibraryName(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(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)
+		}
+	}
+
+	diags := make(map[state.EditorFile][]Diagnostic)
+	for fileName := range a.libs[LibraryName(libraryName)].files {
+		editorFile, err := a.inputFileToEditorFile(fileName)
+		if err == nil {
+			diags[editorFile] = []Diagnostic{}
+		}
+	}
+	if errorsAndWarnings, err := a.fidlcDiagsFromStderr(stderr.Bytes()); err == nil {
+		for editorFile, fileDiags := range errorsAndWarnings {
+			if _, ok := diags[editorFile]; !ok {
+				diags[editorFile] = []Diagnostic{}
+			}
+			diags[editorFile] = append(diags[editorFile], fileDiags...)
+		}
+	}
+	if lints, err := a.runFidlLint(a.inputFilesFIDL[path]); err == nil {
+		for editorFile, fileDiags := range lints {
+			if _, ok := diags[editorFile]; !ok {
+				diags[editorFile] = []Diagnostic{}
+			}
+			diags[editorFile] = append(diags[editorFile], fileDiags...)
+		}
+	}
+
+	return compileResult{
+		lib:   LibraryName(libraryName),
+		diags: diags,
+	}, nil
+}
diff --git a/fidl-lsp/analysis/deps.go b/fidl-lsp/analysis/deps.go
new file mode 100644
index 0000000..ac00e48
--- /dev/null
+++ b/fidl-lsp/analysis/deps.go
@@ -0,0 +1,188 @@
+// 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"
+
+	"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[LibraryName]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  []LibraryName
+	JSON  string
+}
+
+// GetFidlProject builds a CompiledLibraries out of a fidl_project.json file.
+func GetFidlProject() (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[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 []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.EditorFile) ([]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 []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, LibraryName(rootLibraryMatch.Lib))
+	rootImports := state.ParsePlatformImportsMatch(file)
+	for _, m := range rootImports {
+		stack = append(stack, 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 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() ([]LibraryName, error) {
+	var sorted []LibraryName
+	incoming := map[LibraryName]int{}
+	for u := range m {
+		incoming[u] = 0
+	}
+	for _, info := range m {
+		for _, v := range info.deps {
+			incoming[v]++
+		}
+	}
+	var src []LibraryName
+	for u, deg := range incoming {
+		if deg == 0 {
+			src = append(src, u)
+		}
+	}
+	for len(src) != 0 {
+		var u 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..43f6c8e
--- /dev/null
+++ b/fidl-lsp/analysis/deps_test.go
@@ -0,0 +1,262 @@
+// 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.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.OpenFile(
+		"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.CompiledLibraries{
+		analysis.LibraryName("fuchsia.import1"): analysis.CompiledLibrary{
+			Files: []string{"import1.fidl"},
+		},
+		analysis.LibraryName("fuchsia.import2"): analysis.CompiledLibrary{
+			Files: []string{"import2.fidl"},
+		},
+	})
+
+	fs := state.NewFileSystem()
+	fs.OpenFile(
+		"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)+len(fs.Files()))
+	}
+	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.CompiledLibraries{
+		analysis.LibraryName("fuchsia.import1"): analysis.CompiledLibrary{
+			Files: []string{"import1.fidl"},
+			Deps:  []analysis.LibraryName{analysis.LibraryName("fuchsia.import2"), analysis.LibraryName("fuchsia.import3")},
+		},
+		analysis.LibraryName("fuchsia.import2"): analysis.CompiledLibrary{
+			Files: []string{"import2.fidl"},
+			Deps:  []analysis.LibraryName{analysis.LibraryName("fuchsia.import4")},
+		},
+		analysis.LibraryName("fuchsia.import3"): analysis.CompiledLibrary{
+			Files: []string{"import3.fidl"},
+		},
+		analysis.LibraryName("fuchsia.import4"): analysis.CompiledLibrary{
+			Files: []string{"import4.fidl"},
+		},
+	})
+
+	fs := state.NewFileSystem()
+	fs.OpenFile(
+		"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)+len(fs.Files()))
+	}
+	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.CompiledLibraries{
+		analysis.LibraryName("fuchsia.import1"): analysis.CompiledLibrary{
+			Files: []string{"import1_1.fidl", "import1_2.fidl"},
+			Deps:  []analysis.LibraryName{analysis.LibraryName("fuchsia.import3"), analysis.LibraryName("fuchsia.import4")},
+		},
+		analysis.LibraryName("fuchsia.import2"): analysis.CompiledLibrary{
+			Files: []string{"import2_1.fidl", "import2_2.fidl"},
+			Deps:  []analysis.LibraryName{analysis.LibraryName("fuchsia.import4")},
+		},
+		analysis.LibraryName("fuchsia.import3"): analysis.CompiledLibrary{
+			Files: []string{"import3.fidl"},
+		},
+		analysis.LibraryName("fuchsia.import4"): analysis.CompiledLibrary{
+			Files: []string{"import4_1.fidl", "import4_2.fidl", "import4_3.fidl"},
+		},
+	})
+
+	fs := state.NewFileSystem()
+	fs.OpenFile(
+		"test1.fidl",
+		`
+library test;
+using fuchsia.import1; // depends on import2, import3
+`,
+	)
+	fs.OpenFile(
+		"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)+len(fs.Files()))
+	}
+	// 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.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.OpenFile(
+		"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.CompiledLibraries{})
+	fs := state.NewFileSystem()
+	fs.OpenFile(
+		"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.CompiledLibraries{
+		analysis.LibraryName("fuchsia.foo"): analysis.CompiledLibrary{
+			Files: []string{"foo.fidl"},
+			Deps:  []analysis.LibraryName{analysis.LibraryName("fuchsia.bar")},
+		},
+		analysis.LibraryName("fuchsia.bar"): analysis.CompiledLibrary{
+			Files: []string{"bar.fidl"},
+			Deps:  []analysis.LibraryName{analysis.LibraryName("fuchsia.foo")},
+		},
+	})
+
+	fs := state.NewFileSystem()
+	fs.OpenFile(
+		"foo.fidl",
+		`
+library fuchsia.foo;
+using fuchsia.bar;
+`,
+	)
+	fs.OpenFile(
+		"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..0b8a7fd
--- /dev/null
+++ b/fidl-lsp/analysis/diagnostics.go
@@ -0,0 +1,93 @@
+// 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"
+
+	"fidl-lsp/state"
+)
+
+// DiagnosticsOnLibrary retrieves the cached diagnostics for `lib`.
+func (a *Analyzer) DiagnosticsOnLibrary(lib LibraryName) (map[state.EditorFile][]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.EditorFile][]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.EditorFile][]Diagnostic)
+	for i := range diags {
+		editorFile, err := a.inputFileToEditorFile(diags[i].Path)
+		if err != nil {
+			continue
+		}
+		if _, ok := fileToDiags[editorFile]; !ok {
+			fileToDiags[editorFile] = []Diagnostic{}
+		}
+		diags[i].Source = Fidlc
+		fileToDiags[editorFile] = append(fileToDiags[editorFile], diags[i])
+	}
+	return fileToDiags, nil
+}
+
+func (a *Analyzer) runFidlLint(path string) (map[state.EditorFile][]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(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.EditorFile][]Diagnostic)
+	for i := range diags {
+		editorFile, err := a.inputFileToEditorFile(diags[i].Path)
+		if err != nil {
+			continue
+		}
+		if _, ok := fileToDiags[editorFile]; !ok {
+			fileToDiags[editorFile] = []Diagnostic{}
+		}
+		diags[i].Source = FidlLint
+		fileToDiags[editorFile] = append(fileToDiags[editorFile], 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..624f53d
--- /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.EditorFile) (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(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..44c3fee
--- /dev/null
+++ b/fidl-lsp/analysis/library.go
@@ -0,0 +1,159 @@
+// 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         LibraryName
+	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 elementType struct {
+	Kind       string
+	Identifier string `json:"identifier,omitempty"`
+	Subtype    string `json:"subtype,omitempty"`
+}
+
+type typeCtor struct {
+	Name string
+	Args []typeCtor
+}
+
+type declType struct {
+	Kind        TypeKind
+	ElementType elementType `json:"element_type,omitempty"`
+	Identifier  string      `json:"identifier,omitempty"`
+	Subtype     string      `json:"subtype,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
+	MaybeResponse []member
+	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
+	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"`
+}
+
+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/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/diagnostics.go b/fidl-lsp/langserver/diagnostics.go
new file mode 100644
index 0000000..370f957
--- /dev/null
+++ b/fidl-lsp/langserver/diagnostics.go
@@ -0,0 +1,86 @@
+// 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"
+
+	"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.EditorFile(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(analysis.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 editorFile, fileDiags := range diags {
+		fileURI := lsp.DocumentURI(editorFile)
+		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..60b70dd
--- /dev/null
+++ b/fidl-lsp/langserver/files.go
@@ -0,0 +1,44 @@
+// 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.OpenFile(state.EditorFile(params.TextDocument.URI), params.TextDocument.Text)
+}
+
+func (h *LangHandler) handleCloseFile(params lsp.DidCloseTextDocumentParams) {
+	h.fs.CloseFile(state.EditorFile(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,
+				},
+			},
+			RangeLength: lspChange.RangeLength,
+			Text:        lspChange.Text,
+		}
+	}
+
+	if err := h.fs.ApplyChanges(state.EditorFile(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..5cf2aa8
--- /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.EditorFile(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..1cfb0d8
--- /dev/null
+++ b/fidl-lsp/langserver/handler.go
@@ -0,0 +1,224 @@
+// 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"
+
+	"github.com/sourcegraph/go-lsp"
+	"github.com/sourcegraph/jsonrpc2"
+
+	"fidl-lsp/analysis"
+	"fidl-lsp/state"
+)
+
+// LspServer is a wrapper around a LangHandler.
+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      lsp.ServerCapabilities
+	init     *lsp.InitializeParams
+	shutdown bool
+
+	log *log.Logger
+
+	fs *state.FileSystem
+
+	analyzer *analysis.Analyzer
+}
+
+// NewDefaultConfig returns a default configuration for the language server.
+func NewDefaultConfig() lsp.ServerCapabilities {
+	return lsp.ServerCapabilities{
+		DocumentFormattingProvider: true,
+		TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{
+			Options: &lsp.TextDocumentSyncOptions{
+				OpenClose: true,
+				Change:    lsp.TDSKIncremental,
+			},
+		},
+	}
+}
+
+// NewLangHandler returns a LangHandler with the given configuration, logger,
+// and set of CompiledLibraries.
+func NewLangHandler(cfg lsp.ServerCapabilities, trace *log.Logger, fidlProject analysis.CompiledLibraries) *LangHandler {
+	return &LangHandler{
+		log:      trace,
+		cfg:      cfg,
+		fs:       state.NewFileSystem(),
+		analyzer: analysis.NewAnalyzer(fidlProject),
+	}
+}
+
+// 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) {
+	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 lsp.InitializeResult{Capabilities: h.cfg}, 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.EditorFile(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.EditorFile(params.TextDocument.URI)); err != nil {
+			h.log.Println(err)
+		}
+		h.publishDiagnostics(ctx, conn, params.TextDocument.URI)
+		return nil, nil
+
+	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)
+
+	default:
+		return nil, &jsonrpc2.Error{
+			Code:    jsonrpc2.CodeMethodNotFound,
+			Message: fmt.Sprintf("method not supported: %s", req.Method),
+		}
+	}
+}
diff --git a/fidl-lsp/langserver/testutils.go b/fidl-lsp/langserver/testutils.go
new file mode 100644
index 0000000..2eee5de
--- /dev/null
+++ b/fidl-lsp/langserver/testutils.go
@@ -0,0 +1,33 @@
+// 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"
+)
+
+type TestFile struct {
+	Name string
+	Text string
+}
+
+func NewHandlerWithFiles(files ...TestFile) *LangHandler {
+	handler := NewLangHandler(
+		NewDefaultConfig(),
+		log.New(os.Stderr, "[LSP Test] ", log.Lshortfile),
+		analysis.CompiledLibraries{},
+	)
+
+	for _, file := range files {
+		handler.fs.OpenFile(state.EditorFile(file.Name), file.Text)
+		handler.analyzer.Analyze(handler.fs, state.EditorFile(file.Name))
+	}
+
+	return handler
+}
diff --git a/fidl-lsp/main.go b/fidl-lsp/main.go
new file mode 100644
index 0000000..6e2a0bf
--- /dev/null
+++ b/fidl-lsp/main.go
@@ -0,0 +1,61 @@
+// 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"
+)
+
+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)
+
+	fidlProject, err := analysis.GetFidlProject()
+	if err != nil {
+		trace.Fatalf("Failed to parse fidl_project.json: %s\n", err)
+	}
+
+	handler := langserver.NewLangHandler(langserver.NewDefaultConfig(), trace, fidlProject)
+	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/gen_fidl_project.py b/fidl-lsp/scripts/gen_fidl_project.py
new file mode 100644
index 0000000..c01186c
--- /dev/null
+++ b/fidl-lsp/scripts/gen_fidl_project.py
@@ -0,0 +1,102 @@
+#!/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 libaries, 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..39a16db
--- /dev/null
+++ b/fidl-lsp/state/fs.go
@@ -0,0 +1,133 @@
+// 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 (
+	"errors"
+	"fmt"
+	"net/url"
+	"strings"
+)
+
+// An EditorFile 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 EditorFile 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[lsp.DocumentURI][]byte instead?
+	files map[EditorFile]string
+}
+
+// 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.
+type Position struct {
+	Line      int
+	Character int
+}
+
+// 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
+	RangeLength uint
+	Text        string
+}
+
+// NewFileSystem returns an initialized empty FileSystem.
+func NewFileSystem() *FileSystem {
+	return &FileSystem{
+		files: make(map[EditorFile]string),
+	}
+}
+
+// File is a read-only accessor for the in-memory file with ID `path`.
+func (fs *FileSystem) File(path EditorFile) (string, error) {
+	if file, ok := fs.files[path]; ok {
+		return file, nil
+	}
+	return "", fmt.Errorf("file `%s` not in memory", path)
+}
+
+// Files returns all of the in-memory files for traversal.
+func (fs *FileSystem) Files() map[EditorFile]string {
+	return fs.files
+}
+
+// OpenFile copies the contents of the TextDocumentItem into the FileSystem's
+// in-memory file system ("opening" the file).
+func (fs *FileSystem) OpenFile(path EditorFile, text string) {
+	fs.files[path] = text
+}
+
+// CloseFile deletes the in-memory representation of `file`.
+func (fs *FileSystem) CloseFile(path EditorFile) {
+	delete(fs.files, path)
+}
+
+// ApplyChanges applies the specified changes to the relevant in-memory file.
+func (fs *FileSystem) ApplyChanges(path EditorFile, changes []Change) error {
+	for _, change := range changes {
+		start, err := offsetInFile(fs.files[path], change.Range.Start)
+		if err != nil {
+			return err
+		}
+		end := start + change.RangeLength
+
+		// Copy file over to new buffer with inserted text change
+		var newFile strings.Builder
+		newFile.WriteString(fs.files[path][:start])
+		newFile.WriteString(change.Text)
+		newFile.WriteString(fs.files[path][end:])
+		fs.files[path] = newFile.String()
+	}
+	return nil
+}
+
+func offsetInFile(contents string, pos Position) (uint, error) {
+	line := 0
+	col := 0
+	var offset uint = 0
+	for _, c := range contents {
+		if line == pos.Line && col == pos.Character {
+			return offset, nil
+		}
+		offset++
+		if c == '\n' {
+			line++
+			col = 0
+		} else {
+			col++
+		}
+	}
+	// Check if pos is pointing at the end of the file
+	if line == pos.Line && col == pos.Character {
+		return offset, nil
+	}
+	return 0, errors.New("position was out of bounds in file")
+}
+
+// EditorFileToPath converts an EditorFile ID, which is in the form of a file
+// schema URI ("file:///"), to an absolute path.
+func EditorFileToPath(editorFile EditorFile) (string, error) {
+	fileURI, err := url.Parse(string(editorFile))
+	if err != nil {
+		return "", fmt.Errorf("could not parse EditorFile: %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..534ca29
--- /dev/null
+++ b/fidl-lsp/state/fs_test.go
@@ -0,0 +1,304 @@
+// 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.OpenFile("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},
+				},
+				RangeLength: 0,
+				Text:        "tended.library.ex",
+			},
+			{
+				Range: &state.Range{
+					Start: state.Position{Line: 2, Character: 0},
+					End:   state.Position{Line: 2, Character: 0},
+				},
+				RangeLength: 0,
+				Text:        "\nconst uint8 INSERTED_CONST = 0;\n",
+			},
+			{
+				Range: &state.Range{
+					Start: state.Position{Line: 5, Character: 0},
+					End:   state.Position{Line: 5, Character: 0},
+				},
+				RangeLength: 0,
+				Text:        "/// Inserted doc comment\n",
+			},
+			{
+				Range: &state.Range{
+					Start: state.Position{Line: 7, Character: 14},
+					End:   state.Position{Line: 7, Character: 14},
+				},
+				RangeLength: 0,
+				Text:        "\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.OpenFile("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: 5, Character: 3},
+				},
+				RangeLength: 39,
+				Text:        "",
+			},
+		},
+	)
+	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.OpenFile(
+		"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},
+				},
+				RangeLength: 0,
+				Text:        "// 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: 0, Character: 21},
+				},
+				RangeLength: 21,
+				Text:        "",
+			},
+		},
+	)
+	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.OpenFile("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},
+				},
+				RangeLength: 0,
+				Text:        "\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},
+				},
+				RangeLength: 20,
+				Text:        "",
+			},
+		},
+	)
+
+	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.OpenFile("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},
+				},
+				RangeLength: 0,
+				Text:        "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},
+				},
+				RangeLength: 0,
+				Text:        "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..977ce8a
--- /dev/null
+++ b/fidl-lsp/state/parse.go
@@ -0,0 +1,103 @@
+// 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"
+)
+
+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   string
+	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) (string, error) {
+	fidlLib, ok := ParseLibraryMatch(file)
+	if !ok {
+		return "", fmt.Errorf("Could not find library of file '%s'", 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: 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,
+			},
+		},
+	}
+}
diff --git a/fidl-lsp/state/parse_test.go b/fidl-lsp/state/parse_test.go
new file mode 100644
index 0000000..f7a92c8
--- /dev/null
+++ b/fidl-lsp/state/parse_test.go
@@ -0,0 +1,130 @@
+// 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 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 != 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 != 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: "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: "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: "fuchsia.foo.bar",
+			Range: state.Range{
+				Start: state.Position{Line: 2, Character: 6},
+				End:   state.Position{Line: 2, Character: 21},
+			},
+		},
+		{
+			Lib: "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)
+		}
+	}
+}