Support for --_interactive mode of universal-ctags.

For running the tests, a universal-ctags version of
5d82ee6e92ee9cdda5cc9dcee91646dd678b4b42 or newer is needed.

Change-Id: I9aa13237d1045e5169a49bd76aab9a398c4a0c36
diff --git a/ctags/json.go b/ctags/json.go
new file mode 100644
index 0000000..5136521
--- /dev/null
+++ b/ctags/json.go
@@ -0,0 +1,161 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ctags
+
+import (
+	"bufio"
+	"encoding/json"
+	"io"
+	"os"
+	"os/exec"
+	"runtime"
+	"sync"
+)
+
+type ctagsProcess struct {
+	cmd     *exec.Cmd
+	in      io.WriteCloser
+	out     *bufio.Scanner
+	outPipe io.ReadCloser
+
+	procErrMu sync.Mutex
+	procErr   error
+}
+
+var bin = "universal-ctags"
+
+func newProcess() (*ctagsProcess, error) {
+	opt := "default"
+	if runtime.GOOS == "linux" {
+		opt = "sandbox"
+	}
+
+	cmd := exec.Command(bin, "--_interactive="+opt, "--fields=*")
+	in, err := cmd.StdinPipe()
+	if err != nil {
+		return nil, err
+	}
+
+	out, err := cmd.StdoutPipe()
+	if err != nil {
+		in.Close()
+		return nil, err
+	}
+	cmd.Stderr = os.Stderr
+	proc := ctagsProcess{
+		cmd:     cmd,
+		in:      in,
+		out:     bufio.NewScanner(out),
+		outPipe: out,
+	}
+
+	if err := cmd.Start(); err != nil {
+		return nil, err
+	}
+
+	var init reply
+	if err := proc.read(&init); err != nil {
+		return nil, err
+	}
+
+	return &proc, nil
+}
+
+func (p *ctagsProcess) Close() {
+	p.cmd.Process.Kill()
+	p.outPipe.Close()
+	p.in.Close()
+}
+
+func (p *ctagsProcess) read(rep *reply) error {
+	if !p.out.Scan() {
+		// capture exit error.
+		err := p.cmd.Wait()
+		p.outPipe.Close()
+		p.in.Close()
+		return err
+	}
+	return json.Unmarshal(p.out.Bytes(), rep)
+}
+
+func (p *ctagsProcess) post(req *request, content []byte) error {
+	body, err := json.Marshal(req)
+	if err != nil {
+		return err
+	}
+	body = append(body, '\n')
+	if _, err = p.in.Write(body); err != nil {
+		return err
+	}
+	_, err = p.in.Write(content)
+
+	return err
+}
+
+type request struct {
+	Command  string `json:"command"`
+	Filename string `json:"filename"`
+	Size     int    `json:"size"`
+}
+
+type reply struct {
+	// Init
+	Typ     string `json:"_type"`
+	Name    string `json:"name"`
+	Version string `json:"version"`
+
+	// completed
+	Command string `json:"command"`
+
+	Path     string `json:"path"`
+	Pattern  string `json:"pattern"`
+	Language string `json:"language"`
+	Line     int    `json:"line"`
+	Kind     string `json:"kind"`
+}
+
+func (p *ctagsProcess) Process(name string, content []byte) ([]*Entry, error) {
+	req := request{
+		Command:  "generate-tags",
+		Size:     len(content),
+		Filename: name,
+	}
+
+	if err := p.post(&req, content); err != nil {
+		return nil, err
+	}
+
+	var es []*Entry
+	for {
+		var rep reply
+		if err := p.read(&rep); err != nil {
+			return nil, err
+		}
+		if rep.Typ == "completed" {
+			break
+		}
+
+		e := Entry{
+			Sym:  rep.Name,
+			Path: rep.Path,
+			Line: rep.Line,
+			Kind: rep.Kind,
+		}
+
+		es = append(es, &e)
+	}
+
+	return es, nil
+}
diff --git a/ctags/json_test.go b/ctags/json_test.go
new file mode 100644
index 0000000..f42febe
--- /dev/null
+++ b/ctags/json_test.go
@@ -0,0 +1,94 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ctags
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestJSON(t *testing.T) {
+	p, err := newProcess()
+	if err != nil {
+		t.Fatal("newProcess", err)
+	}
+
+	defer p.Close()
+
+	java := `
+package io.zoekt;
+import java.util.concurrent.Future;
+class Back implements Future extends Frob {
+  public static int BLA = 1;
+  public int member;
+  public Back() {
+    member = 2;
+  }
+  public int method() {
+    member++;
+  }
+}
+`
+	name := "io/zoekt/Back.java"
+	got, err := p.Process(name, []byte(java))
+	if err != nil {
+		t.Errorf("Process: %v", err)
+	}
+
+	want := []*Entry{
+		{
+			Sym:  "io.zoekt",
+			Kind: "package",
+			Path: "io/zoekt/Back.java",
+			Line: 2,
+		},
+		{
+			Sym:  "Back",
+			Path: "io/zoekt/Back.java",
+			Line: 4,
+			Kind: "class",
+		},
+
+		{
+			Sym:  "BLA",
+			Path: "io/zoekt/Back.java",
+			Line: 5,
+			Kind: "field",
+		},
+		{
+			Sym:  "member",
+			Path: "io/zoekt/Back.java",
+			Line: 6,
+			Kind: "field",
+		},
+		{
+			Sym:  "Back",
+			Path: "io/zoekt/Back.java",
+			Line: 7,
+			Kind: "method",
+		},
+		{
+			Sym:  "method",
+			Path: "io/zoekt/Back.java",
+			Line: 10, Kind: "method",
+		},
+	}
+
+	for i := range want {
+		if !reflect.DeepEqual(got[i], want[i]) {
+			t.Fatalf("got %#v, want %#v", got[i], want[i])
+		}
+	}
+}
diff --git a/ctags/parse.go b/ctags/parse.go
index 3d8b9ec..b8783b1 100644
--- a/ctags/parse.go
+++ b/ctags/parse.go
@@ -28,10 +28,7 @@
 	Parent     string
 	ParentType string
 
-	FileLimited    bool
-	Access         string
-	Implementation bool
-	Inherits       []string
+	FileLimited bool
 }
 
 // Parse parses a single line of exuberant "ctags -n" output.